// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import path from 'node:path';
import fs from 'node:fs';
import { Async, FileSystem, Path } from '@rushstack/node-core-library';
import { ArchiveManager } from './ArchiveManager';
import { remapSourcePathForTargetFolder } from './Utils';
export class AssetHandler {
    constructor(options) {
        this._includedAssetPaths = new Set();
        this._isFinalized = false;
        const { terminal, sourceRootFolder, targetRootFolder, linkCreation, symlinkAnalyzer, createArchiveFilePath, createArchiveOnly = false } = options;
        this._terminal = terminal;
        this._sourceRootFolder = sourceRootFolder;
        this._targetRootFolder = targetRootFolder;
        this._symlinkAnalyzer = symlinkAnalyzer;
        if (createArchiveFilePath) {
            if (path.extname(createArchiveFilePath) !== '.zip') {
                throw new Error('Only archives with the .zip file extension are currently supported.');
            }
            this._archiveFilePath = path.resolve(targetRootFolder, createArchiveFilePath);
            this._archiveManager = new ArchiveManager();
        }
        if (createArchiveOnly && !this._archiveManager) {
            throw new Error('createArchiveOnly cannot be true if createArchiveFilePath is not provided');
        }
        this._createArchiveOnly = createArchiveOnly;
        this._linkCreationMode = linkCreation || 'default';
    }
    async includeAssetAsync(options) {
        const { sourceFileContent, targetFilePath, ignoreIfExisting = false } = options;
        let { sourceFilePath } = options;
        if (this._isFinalized) {
            throw new Error('includeAssetAsync() cannot be called after finalizeAsync()');
        }
        if (!sourceFilePath && !sourceFileContent) {
            if (!Path.isUnder(targetFilePath, this._targetRootFolder)) {
                throw new Error('The existing asset path must be under the target root folder');
            }
            sourceFilePath = targetFilePath;
        }
        if (sourceFilePath && sourceFileContent) {
            throw new Error('Either sourceFilePath or sourceFileContent must be provided, but not both');
        }
        if (this._includedAssetPaths.has(targetFilePath)) {
            if (ignoreIfExisting) {
                return;
            }
            throw new Error(`The asset at path "${targetFilePath}" has already been included`);
        }
        if (!this._createArchiveOnly) {
            // Ignore when the source file is the same as the target file, as it's a no-op
            if (sourceFilePath && sourceFilePath !== targetFilePath) {
                // Use the fs.copyFile API instead of FileSystem.copyFileAsync() since copyFileAsync performs
                // a needless stat() call to determine if it's a file or folder, and we already know it's a file.
                try {
                    await fs.promises.copyFile(sourceFilePath, targetFilePath, fs.constants.COPYFILE_EXCL);
                }
                catch (e) {
                    if (!FileSystem.isNotExistError(e)) {
                        throw e;
                    }
                    // The parent folder may not exist, so ensure it exists before trying to copy again
                    await FileSystem.ensureFolderAsync(path.dirname(targetFilePath));
                    await fs.promises.copyFile(sourceFilePath, targetFilePath, fs.constants.COPYFILE_EXCL);
                }
            }
            else if (sourceFileContent) {
                await FileSystem.writeFileAsync(targetFilePath, sourceFileContent, {
                    ensureFolderExists: true
                });
            }
        }
        if (this._archiveManager) {
            const targetRelativeFilePath = path.relative(this._targetRootFolder, targetFilePath);
            if (sourceFilePath) {
                await this._archiveManager.addToArchiveAsync({
                    filePath: sourceFilePath,
                    archivePath: targetRelativeFilePath
                });
            }
            else if (sourceFileContent) {
                await this._archiveManager.addToArchiveAsync({
                    fileData: sourceFileContent,
                    archivePath: targetRelativeFilePath
                });
            }
        }
        this._includedAssetPaths.add(targetFilePath);
    }
    get assetPaths() {
        return [...this._includedAssetPaths];
    }
    async finalizeAsync(options) {
        const { onAfterExtractSymlinksAsync } = options !== null && options !== void 0 ? options : {};
        if (this._isFinalized) {
            throw new Error('finalizeAsync() has already been called');
        }
        if (this._linkCreationMode === 'default') {
            this._terminal.writeLine('Creating symlinks');
            const linksToCopy = this._symlinkAnalyzer.reportSymlinks();
            await Async.forEachAsync(linksToCopy, async (linkToCopy) => {
                await this._extractSymlinkAsync(linkToCopy);
            });
        }
        await (onAfterExtractSymlinksAsync === null || onAfterExtractSymlinksAsync === void 0 ? void 0 : onAfterExtractSymlinksAsync());
        if (this._archiveManager && this._archiveFilePath) {
            this._terminal.writeLine(`Creating archive at "${this._archiveFilePath}"`);
            await this._archiveManager.createArchiveAsync(this._archiveFilePath);
        }
        this._isFinalized = true;
    }
    /**
     * Create a symlink as described by the ILinkInfo object.
     */
    async _extractSymlinkAsync(linkInfo) {
        const { kind, linkPath, targetPath } = {
            ...linkInfo,
            linkPath: remapSourcePathForTargetFolder({
                sourceRootFolder: this._sourceRootFolder,
                targetRootFolder: this._targetRootFolder,
                sourcePath: linkInfo.linkPath
            }),
            targetPath: remapSourcePathForTargetFolder({
                sourceRootFolder: this._sourceRootFolder,
                targetRootFolder: this._targetRootFolder,
                sourcePath: linkInfo.targetPath
            })
        };
        const newLinkFolder = path.dirname(linkPath);
        await FileSystem.ensureFolderAsync(newLinkFolder);
        // Link to the relative path for symlinks
        const relativeTargetPath = path.relative(newLinkFolder, targetPath);
        // NOTE: This logic is based on NpmLinkManager._createSymlink()
        if (kind === 'fileLink') {
            // For files, we use a Windows "hard link", because creating a symbolic link requires
            // administrator permission. However hard links seem to cause build failures on Mac,
            // so for all other operating systems we use symbolic links for this case.
            if (process.platform === 'win32') {
                await FileSystem.createHardLinkAsync({
                    linkTargetPath: relativeTargetPath,
                    newLinkPath: linkPath
                });
            }
            else {
                await FileSystem.createSymbolicLinkFileAsync({
                    linkTargetPath: relativeTargetPath,
                    newLinkPath: linkPath
                });
            }
        }
        else {
            // Junctions are only supported on Windows. This will create a symbolic link on other platforms.
            await FileSystem.createSymbolicLinkJunctionAsync({
                linkTargetPath: relativeTargetPath,
                newLinkPath: linkPath
            });
        }
        // Since the created symlinks have the required relative paths, they can be added directly to
        // the archive.
        await this.includeAssetAsync({ targetFilePath: linkPath });
    }
}
//# sourceMappingURL=AssetHandler.js.map