"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Orchestrator = void 0;
const sdk_build_core_1 = require("@servicenow/sdk-build-core");
const sdk_build_plugins_1 = require("@servicenow/sdk-build-plugins");
const flow_activation_1 = require("./flow-activation");
const snakeCase_1 = __importDefault(require("lodash/snakeCase"));
const os = __importStar(require("os"));
const connector_1 = require("./connector");
const credentials_1 = require("./credentials");
const dependencies_1 = require("./orchestrator/dependencies");
const relationship_resolver_1 = require("./orchestrator/dependencies/relationship-resolver");
const uninstaller_1 = require("./orchestrator/uninstaller");
const post_install_tasks_1 = require("./post-install-tasks");
const project_factory_1 = require("./project-factory");
const util_1 = require("./util");
const UPDATE_XML_TRANSFORM_LIMIT_DEFAULT = 2000;
/**
 * The Orchestrator class is responsible for managing and coordinating the lifecycle of
 * a project, including the build, packaging, and installation processes.
 */
class Orchestrator {
    project;
    telemetry;
    credential;
    connector;
    constructor(project, credentialOrConnector) {
        this.project = project;
        this.telemetry = project.getTelemetryClient() ?? sdk_build_core_1.TelemetryFactory.create();
        if (credentialOrConnector instanceof credentials_1.LazyCredential) {
            this.credential = credentialOrConnector;
            this.connector = new connector_1.Connector(credentialOrConnector);
        }
        else if (credentialOrConnector) {
            this.connector = credentialOrConnector;
        }
    }
    getConnector() {
        if (!this.connector) {
            throw new Error('No credentials or connector provided to Orchestrator. Operations requiring instance access need authentication.');
        }
        return this.connector;
    }
    /**
     * Build the project, generate the output files, and save the keys registry.
     *
     * @param frozenKeys If true, an error will be thrown if any changes to the keys registry are detected.
     * @param skipClean If true, the build output directories will not be cleaned before building.
     * @param sysIds If provided, build output will only include records matching this array
     * @param errorOnConflict If true, sys_id conflicts between Fluent and XML will cause a build error instead of a warning
     * @returns The output files.
     */
    async build(options = {}) {
        const telemetryTimer = this.telemetry.startTimerEvent('build');
        let metricsData;
        try {
            const { frozenKeys = false, errorOnConflict = false, skipClean = false } = options;
            if (!skipClean) {
                this.clean();
            }
            else {
                this.project.getLogger().info('Skipping cleaning of build output directories');
            }
            const logger = this.project.getLogger();
            const config = this.project.getConfig();
            await (0, util_1.uiPrebuild)({ project: this.project });
            const keysFile = this.project.getKeys().getSourceFile();
            const keysContentBefore = keysFile.getFullText();
            const buildIdsSet = options.sysIds ? new Set(options.sysIds) : undefined;
            let files;
            try {
                files = await this.project.saveOutput(buildIdsSet, errorOnConflict);
            }
            catch (e) {
                logger.error(e);
                metricsData = [
                    telemetryTimer.end({
                        appInfo: { scopeId: config.scopeId },
                        data: {
                            success: false,
                            scopeId: config.scope,
                        },
                    }),
                ];
                return {
                    success: false,
                    diagnostics: [],
                };
            }
            this.project.getPlugins().reload();
            if (frozenKeys) {
                const keysContentAfter = keysFile.getFullText();
                // In CI Mode, we want to ensure that the keys file is up-to-date.
                // Keys.ts file is reconstructed during build process even if there are no changes
                // to the keys file, that's why we need to compare the content of the keys file
                if (keysContentBefore !== keysContentAfter) {
                    throw new Error('Keys file is out-of-date. To update it, run the build again without frozen keys.');
                }
            }
            await this.project.getKeys().getSourceFile().save(); // Saves the keys file
            await (0, util_1.uiPostbuild)({ project: this.project });
            metricsData = [
                telemetryTimer.end({
                    appInfo: { scopeId: config.scopeId },
                    data: {
                        success: true,
                        scopeId: config.scopeId,
                    },
                }),
            ];
            return {
                success: true,
                files,
                packOutput: this.project.resolvePath(config.packOutputDir), // TODO: Remove after 4.0
                diagnostics: [],
            };
        }
        catch (error) {
            this.telemetry.error('build', error);
            throw error;
        }
        finally {
            if (metricsData) {
                this.telemetry.sendEvent(metricsData);
            }
        }
    }
    /**
     * Cleans the build appOutDir and packOutputDir of the project.
     */
    clean() {
        const { appOutputDir, packOutputDir, staticContent } = this.project.getConfig();
        const workingDir = this.project.getRootDir();
        const fs = this.project.getFileSystem();
        /**
         * Clean REPACK_OUTPUT_DIR at the beginning of the build so,
         * - DEF0676063 - we don't package stale data from older builds
         * - DEF0702536 - we don't clean up the build files between subsequent `toRecord` calls inside server-module plugin
         */
        fs.rmSync(sdk_build_core_1.path.join(workingDir, sdk_build_plugins_1.REPACK_OUTPUT_DIR), {
            recursive: true,
            force: true,
        });
        fs.rmSync(sdk_build_core_1.path.join(workingDir, appOutputDir), {
            recursive: true,
            force: true,
        });
        fs.rmSync(sdk_build_core_1.path.join(workingDir, packOutputDir), {
            recursive: true,
            force: true,
        });
        fs.rmSync(sdk_build_core_1.path.join(workingDir, staticContent.buildDir), {
            recursive: true,
            force: true,
        });
    }
    /**
     * Generate a ZIP archive containing the project's output which can be installed with `install` method
     * @param packagePath The path where the ZIP archive will be saved. Defaults to `packOutputDir/{package name_version}.zip`.
     * @param options.skipPackageInventory If true, skips generating package_inventory.csv
     * @returns The path to the generated ZIP archive.
     */
    async pack(packagePath = this.project.resolvePath(this.project.getConfig().packOutputDir, `${(0, snakeCase_1.default)(`${this.project.getPackage().name}_${this.project.getPackage().version}`)}.zip`), options) {
        const config = this.project.getConfig();
        const { appOutputDir, scope, scopeId } = config;
        const workingDir = this.project.getRootDir();
        const pkg = this.project.getPackage();
        const logger = this.project.getLogger();
        logger.debug(`packing directory ${appOutputDir} to ${packagePath}`);
        const fs = this.project.getFileSystem();
        if (sdk_build_core_1.FileSystem.existsSync(fs, packagePath)) {
            fs.rmSync(packagePath, { recursive: true, force: true });
        }
        fs.mkdirSync(sdk_build_core_1.path.dirname(packagePath), { recursive: true });
        const appOutputPath = sdk_build_core_1.path.join(workingDir, appOutputDir);
        // Check directory has content before generating inventory (matches zipDirectory error)
        if (!sdk_build_core_1.FileSystem.existsSync(fs, appOutputPath) || fs.readdirSync(appOutputPath).length === 0) {
            // eslint-disable-next-line no-throw-literal
            throw `No files found in ${appOutputPath} directory to create zip.`;
        }
        // Generate package_inventory.csv with SHA-256 hashes before zipping
        if (!options?.skipPackageInventory) {
            const build = scope === 'global' ? scopeId : scope;
            if (!build) {
                throw new Error('Cannot generate package_inventory.csv: missing scope or scopeId in config');
            }
            const inventoryContent = await (0, sdk_build_core_1.generatePackageInventoryFromDirectory)(fs, appOutputPath, {
                build,
                type: scope === 'global' ? 'global' : 'scoped',
                appVersion: pkg.version,
                // package_inventory format version (only one version exists)
                version: '1.0.0',
            });
            fs.writeFileSync(sdk_build_core_1.path.join(appOutputPath, 'package_inventory.csv'), inventoryContent);
        }
        await (0, sdk_build_core_1.zipDirectory)(fs, appOutputPath, packagePath);
        return packagePath;
    }
    /**
     * Unpacks a ZIP archive containing ServiceNow application files
     *
     * @param zipPath The path to the ZIP archive.
     * @param targetPath The path where the files will be extracted.
     * @returns The list of files extracted from the ZIP archive.
     */
    unpack(zipPath, targetPath) {
        const fs = this.project.getFileSystem();
        const logger = this.project.getLogger();
        logger.debug(`unpacking directory ${zipPath} to ${targetPath}`);
        // Apply the same excludeFilePatterns filtering as the transform pipeline.
        const patterns = this.project.getConfig().excludeFilePatterns;
        const ignore = patterns.length > 0 ? (f) => (0, sdk_build_core_1.matchesGlob)(f, patterns) : undefined;
        return (0, sdk_build_core_1.unzipAppPackage)(fs, zipPath, targetPath, logger, ignore);
    }
    async transform0(options) {
        let files = [];
        let tempMetaFiles;
        let tmpHostedFileDir;
        const fileSystem = this.project.getFileSystem();
        const config = this.project.getConfig();
        if ('updateSetId' in options) {
            tempMetaFiles = (0, util_1.defaultDownloadDir)();
            const updateCount = await this.getConnector().getUpdateXMLCount(options.updateSetId);
            const updateLimit = options.maxUpdateCount ?? UPDATE_XML_TRANSFORM_LIMIT_DEFAULT;
            if (updateCount > updateLimit) {
                throw new Error(`Update Set contains more than ${updateLimit} records (${updateCount}). To transform this many updates, please supply a higher limit using '--maxUpdateCount'`);
            }
            const updateSet = await this.download(options, tempMetaFiles);
            files = [updateSet];
        }
        else if ('tables' in options) {
            // Table-based transform (filters by table hierarchy)
            const resolved = this.resolveTableTransformFiles(options);
            if (!resolved) {
                return { changedFiles: [], handledPaths: [] };
            }
            files = resolved.files;
            options = { ...options, tables: resolved.tables };
        }
        else if ('paths' in options) {
            files = Array.isArray(options.paths) ? options.paths : [options.paths];
        }
        else if ('sysId' in options || 'sysIds' in options || 'records' in options) {
            let metadataFiles = [];
            if ('table' in options || 'records' in options) {
                const metadataRecords = 'table' in options ? [options] : options.records;
                const appDependencies = new dependencies_1.ApplicationDependencies(this.project, this.getConnector());
                metadataFiles = await appDependencies.fetchRelatedRecords(metadataRecords);
            }
            else {
                metadataFiles = await this.getConnector().unloadRecords(options.sysIds, config.scopeId);
                if (metadataFiles.length === 0) {
                    throw new Error(`No records found on instance in scope for provided sysIds`);
                }
                else if (metadataFiles.length !== options.sysIds.length) {
                    this.project
                        .getLogger()
                        .warn(`Failed to download records for the following IDs: ${options.sysIds.filter((id) => !metadataFiles.find((r) => r.sysId === id)).join(', ')}`);
                }
            }
            tempMetaFiles = (0, util_1.defaultDownloadDir)();
            sdk_build_core_1.FileSystem.ensureDirSync(fileSystem, tempMetaFiles);
            metadataFiles.forEach((file) => {
                const filePath = sdk_build_core_1.path.join(tempMetaFiles, `${file.tableName}_${file.sysId}.xml`);
                fileSystem.writeFileSync(filePath, file.content);
                files.push(filePath);
            });
        }
        else if (config.type === 'configuration') {
            throw new Error('Transform/Sync for a configuration project must provide either table and id of records to transform, or an updateSet ID');
        }
        else {
            const metadataPack = await this.download('method' in options ? options : undefined);
            tempMetaFiles = (0, util_1.defaultDownloadDir)();
            const metadataFiles = await this.unpack(metadataPack, tempMetaFiles);
            fileSystem.rmSync(metadataPack, { recursive: true, force: true });
            files = metadataFiles.files;
            if (config.hostedPlugins) {
                const { files: hostedFiles, tmpDir } = await this.downloadHostedFiles(config, fileSystem);
                tmpHostedFileDir = tmpDir;
                files.push(...hostedFiles);
            }
        }
        const transformResult = await this.project.transform(files, options);
        if (tempMetaFiles) {
            fileSystem.rmSync(tempMetaFiles, { recursive: true, force: true });
            if (tmpHostedFileDir) {
                fileSystem.rmSync(tmpHostedFileDir, { recursive: true, force: true });
            }
        }
        else {
            for (const path of transformResult.handledPaths) {
                // Delete xml sources only if transforming from path in metadata dir
                if (this.project.isInMetadataDir(path)) {
                    this.project.removeFile(path);
                    fileSystem.rmSync(path, { recursive: true, force: true });
                }
            }
        }
        this.project.save();
        return transformResult;
    }
    /**
     * Transforms records filtered by table.
     * All XML files are parsed, but only records belonging to the target tables are transformed.
     *
     * When `options.tables` is set, only those tables (and their descendants) are transformed.
     * Otherwise every plugin root table is processed.
     *
     * Requires `options.paths` — local transform only.
     */
    resolveTableTransformFiles(options) {
        const logger = this.project.getLogger();
        const resolver = new dependencies_1.RelationshipResolver(this.project);
        const tablesToTransform = this.resolveTargetTables(options.tables, resolver, options.force);
        if (tablesToTransform.size === 0) {
            logger.info('Table-based transform: no tables to process');
            return;
        }
        const allFiles = this.collectLocalXmlFiles(options.paths);
        if (allFiles.length === 0) {
            logger.info('Table-based transform: no XML files found');
            return;
        }
        logger.info(`Table-based transform: ${allFiles.length} XML file(s), filtering to ${tablesToTransform.size} table(s)`);
        return { files: allFiles, tables: tablesToTransform };
    }
    /**
     * Resolves the set of tables to transform.
     *
     * When `requestedTables` is given (--table flag), each requested table is
     * treated as a root and only its descendants are included. This prevents
     * unintended expansion to parent or sibling tables.
     *
     * When no tables are specified, all plugin roots and their descendants
     * are included.
     */
    resolveTargetTables(requestedTables, resolver, force) {
        const tables = new Set();
        // When tables are explicitly requested, treat each as a root and only traverse downward
        if (requestedTables?.length) {
            const descendantErrors = [];
            for (const table of requestedTables) {
                // Check if this table is not a plugin root (has parent tables)
                if (!resolver.isPluginRootTable(table)) {
                    const pluginRoots = resolver.getPluginRootTables(table);
                    // Only error if none of the roots are also in the requested tables
                    const rootInRequest = [...pluginRoots].some((root) => requestedTables.includes(root));
                    if (pluginRoots.size > 0 && !force && !rootInRequest) {
                        const rootList = [...pluginRoots].join(', ');
                        descendantErrors.push(`"${table}" is a descendant of: ${rootList}`);
                    }
                }
                // Treat the requested table as root, only traverse downward to descendants
                const hierarchy = resolver.generateRelationshipHierarchy(table);
                for (const t of (0, relationship_resolver_1.collectTablesFromHierarchy)(hierarchy)) {
                    tables.add(t);
                }
            }
            if (descendantErrors.length > 0) {
                throw new Error(`Cannot transform descendant tables directly. Pass the root table(s) instead, or use --force to proceed:\n  ${descendantErrors.join('\n  ')}`);
            }
            return tables;
        }
        // No tables specified: process all plugin roots with full relationship resolution
        const sourceTables = [...this.project.getPlugins()].flatMap((plugin) => [...(0, relationship_resolver_1.getRootTables)(plugin)]);
        for (const table of sourceTables) {
            const hierarchies = resolver.resolveRelationship(table);
            if (hierarchies.length === 0) {
                tables.add(table);
                continue;
            }
            for (const hierarchy of hierarchies) {
                for (const t of (0, relationship_resolver_1.collectTablesFromHierarchy)(hierarchy)) {
                    tables.add(t);
                }
            }
        }
        return tables;
    }
    collectLocalXmlFiles(inputPaths) {
        const fileSystem = this.project.getFileSystem();
        const paths = Array.isArray(inputPaths) ? inputPaths : [inputPaths];
        const xmlFiles = [];
        for (const inputPath of paths) {
            const absolutePath = sdk_build_core_1.path.isAbsolute(inputPath)
                ? inputPath
                : sdk_build_core_1.path.join(this.project.getRootDir(), inputPath);
            if (fileSystem.statSync(absolutePath).isDirectory()) {
                (0, util_1.traverseDirectory)(fileSystem, absolutePath, {
                    extensions: ['.xml'],
                    visitor: (filePath) => xmlFiles.push(filePath),
                });
            }
            else {
                xmlFiles.push(absolutePath);
            }
        }
        return xmlFiles;
    }
    /**
     * Transforms the project's metadata files.
     *
     * @param options Options for the transform process
     * @returns The transformed files
     */
    async transform(options) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('transform');
            const config = this.project.getConfig();
            const transformResult = await this.transform0(options ?? {});
            this.telemetry.sendEvent(telemetryTimer.end({
                appInfo: { scopeId: config.scopeId },
                data: {
                    transformedMetadataCount: transformResult ? transformResult.changedFiles.length : 0,
                    mode: options && 'method' in options ? options.method : undefined,
                    local: options && 'paths' in options ? !!options.paths : false,
                    updateSet: options && 'updateSetId' in options,
                },
            }));
            return transformResult;
        }
        catch (error) {
            this.telemetry.error('transform', error);
            throw error;
        }
    }
    async uploadXMLFiles(filePaths, options) {
        const connector = this.getConnector();
        const fs = this.project.getFileSystem();
        const { scopeId } = this.project.getConfig();
        const files = [];
        for (const filePath of filePaths) {
            if (sdk_build_core_1.FileSystem.existsSync(fs, filePath)) {
                const fileStat = fs.statSync(filePath);
                if (fileStat.isFile()) {
                    files.push({
                        blob: new Blob([fs.readFileSync(filePath)], {
                            type: 'application/xml',
                        }),
                        name: sdk_build_core_1.path.basename(filePath),
                    });
                }
                else {
                    throw new Error(`Expected file, received directory: ${filePath}.`);
                }
            }
            else {
                throw new Error(`File ${filePath} does not exist.`);
            }
        }
        return await connector.uploadXmlFiles(scopeId, files, options);
    }
    async downloadHostedFiles(config, fileSystem) {
        if (!config.hostedPlugins) {
            return { files: [], tmpDir: undefined };
        }
        const files = [];
        const hostedFilesFromInstance = [];
        for (const [pluginId, pluginScope] of Object.entries(config.hostedPlugins)) {
            const hostedFilesForPlugin = this.project
                .getFiles()
                .filter((file) => file.isInFluentHostedPluginDir(pluginId));
            if (hostedFilesForPlugin.length > 0) {
                const hostedRecords = (await Promise.all(hostedFilesForPlugin.map(async (f) => (await f.getRecords()).query().map((record) => record.getId().getValue())))).flat();
                if (hostedRecords.length > 0) {
                    const pluginScopeId = pluginScope === 'global'
                        ? 'global'
                        : (await this.getConnector().getScopeInfo(pluginScope))?.appId;
                    if (pluginScopeId) {
                        hostedFilesFromInstance.push(...(await this.getConnector().unloadRecords(hostedRecords, pluginScopeId)));
                    }
                }
            }
        }
        let tempMetaFiles;
        if (hostedFilesFromInstance.length > 0) {
            tempMetaFiles = (0, util_1.defaultDownloadDir)();
            sdk_build_core_1.FileSystem.ensureDirSync(fileSystem, tempMetaFiles);
            hostedFilesFromInstance.forEach((file) => {
                const filePath = sdk_build_core_1.path.join(tempMetaFiles, `${file.tableName}_${file.sysId}.xml`);
                fileSystem.writeFileSync(filePath, file.content);
                files.push(filePath);
            });
        }
        return { files, tmpDir: tempMetaFiles ?? undefined };
    }
    async runPostInstallTasks(installType, skipFlags) {
        const connector = this.getConnector();
        const logger = this.project.getLogger();
        const { scopeId } = this.project.getConfig();
        const recordIds = (0, flow_activation_1.getRecordIdsByTable)(this.project.getKeys().getValue());
        for (const task of post_install_tasks_1.POST_INSTALL_TASKS) {
            if (task.runFor !== installType && task.runFor !== 'both') {
                continue;
            }
            if (skipFlags?.[task.skipFlag]) {
                logger.debug(`Skipping post-install task "${task.name}" (--${(0, snakeCase_1.default)(task.skipFlag).replace(/_/g, '-')})`);
                continue;
            }
            const taskTimer = this.telemetry.startTimerEvent(`post.install.${task.name}`);
            try {
                await task.run({
                    instanceClient: connector,
                    logger,
                    config: this.project.getConfig(),
                    recordIds,
                });
                this.telemetry.sendEvent(taskTimer.end({
                    appInfo: { scopeId },
                    data: { success: true, installType },
                }));
            }
            catch (taskError) {
                this.telemetry.sendEvent(taskTimer.end({
                    appInfo: { scopeId },
                    data: { success: false, installType },
                }));
                logger.debug(`Post-install task "${task.name}" failed: ${taskError instanceof Error ? taskError.message : taskError}`);
            }
        }
    }
    /**
     * Install the project on the connected ServiceNow instance.
     *
     * @param clean If true, the app will be removed from the instance before being installed again.
     * @param packageZipPath The path to the package zip file to install. If not provided, the project will be packed first.
     */
    async install(options) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('install');
            const { scope, scopeId, hostedPlugins, installAs } = this.project.getConfig();
            const { version } = this.project.getPackage();
            const { id: previousInstallId } = await this.installStatus();
            const connector = this.getConnector();
            const scopeInfo = await connector.getScopeInfo(scope, scopeId);
            const isStoreApp = scopeInfo?.sysClassName === 'sys_store_app';
            const hasHostedContent = !!hostedPlugins;
            const logger = this.project.getLogger();
            const wantsStoreApp = !!options?.installAsStoreApp || hasHostedContent || installAs === 'store';
            if (scopeInfo && !options?.clean && wantsStoreApp !== isStoreApp) {
                if (scopeInfo.sysClassName === 'sys_app' && (hasHostedContent || installAs === 'store')) {
                    throw new Error(`Attempting to install app with hosted_plugin content which requires 'sys_store_app' deployment, but there is already a 'sys_app' record for this scope present on the instance. Install with flag '--reinstall' (-r) to delete existing app and reinstall`);
                }
                else if (!hostedPlugins) {
                    throw new Error(`Attempting to install ${options?.installAsStoreApp ? 'a store app' : 'an app'} record but there is already a ${isStoreApp ? 'sys_store_app' : 'sys_app'} record for this scope present on the instance. Install with flag '--reinstall' (-r) to delete existing app and reinstall`);
                }
            }
            // if the app is not installed, we can skip the uninstall step
            if (scopeInfo && options?.clean) {
                logger.info(`Uninstalling application ${scopeInfo?.appId ?? ''} for clean installation...`);
                logger.warn(`This operation may cause unintended removal of metadata created by other developers on your instance. When reinstalling, any application metadata on your instance that isn't present in the local application installation package generated by $now-sdk build will be removed during the uninstall process. If there is metadata that's out of sync with your local application, use $now-sdk transform to synchronize your local metadata with the metadata on your instance.`);
                await new uninstaller_1.Uninstaller(connector, logger).process(scope, scopeId, isStoreApp, hasHostedContent);
            }
            let packageZipPath = options?.packageZipPath;
            if (!packageZipPath) {
                packageZipPath = await this.pack();
            }
            const fs = this.project.getFileSystem();
            if (!sdk_build_core_1.FileSystem.existsSync(fs, packageZipPath)) {
                throw new Error(`Installation failed due to missing installation artifact '${packageZipPath}'. Run $now-sdk build before running $now-sdk install to generate a build artifact for installation.`);
            }
            const buffer = fs.readFileSync(packageZipPath);
            const file = new Blob([new Uint8Array(buffer)]);
            if (hasHostedContent) {
                logger.warn(`Hosted content will not be reloaded when installing the same version on older family release. To install updates to hosted content, please reinstall the app with '-r' flag`);
            }
            logger.debug(`installing package ${packageZipPath}`);
            const isStoreAppInstall = isStoreApp || wantsStoreApp;
            const result = await connector.uploadScopedAppPackage(file, {
                loadDemoData: options?.demoData ?? true,
                isStoreApp: isStoreAppInstall,
                registerScope: false, // FIXME: Does not work on earlier glide builds
                installAsync: options?.installAsync ?? false,
                appInfo: {
                    scope,
                    scopeId,
                    version,
                },
            });
            if (result?.trackerId) {
                let installSuccessful = false;
                let errorMessage = '';
                let attempt = 0;
                while (true) {
                    const { status, status_message } = await connector.getProgress(result?.trackerId);
                    if (status === uninstaller_1.TrackerStatus.ERROR || status === uninstaller_1.TrackerStatus.SUCCESSFUL) {
                        installSuccessful = status === uninstaller_1.TrackerStatus.SUCCESSFUL;
                        errorMessage = status_message;
                        break;
                    }
                    await new Promise((resolve) => setTimeout(resolve, 1000));
                    attempt += 1;
                    if (attempt % 30 === 0) {
                        logger.info(`App install pending${status_message ? `, waiting on ${status_message}` : '...'}`);
                    }
                }
                if (installSuccessful) {
                    logger.info(`Rollback (undo installation): ${connector.getHost().origin}/sys_rollback_context.do?sys_id=${result.rollbackId}`);
                }
                else {
                    throw new Error(errorMessage);
                }
            }
            else {
                let checkCount = 0;
                let installStatus;
                while (checkCount < 30) {
                    installStatus = await this.installStatus();
                    if (installStatus?.finished && installStatus.id !== previousInstallId) {
                        break;
                    }
                    await new Promise((resolve) => setTimeout(resolve, 1000));
                    checkCount++;
                }
                if (!installStatus || !installStatus.finished || installStatus.id === previousInstallId) {
                    throw new Error(`Could not determine app installation status. Check instance to verify installation.`);
                }
            }
            await this.runPostInstallTasks('app', options?.skipFlags);
            this.telemetry.sendEvent(telemetryTimer.end({
                data: {
                    scopeId,
                    installed: true,
                    isStoreAppInstall,
                },
            }));
            return { trackerId: result?.trackerId, rollbackId: result?.rollbackId };
        }
        catch (error) {
            this.telemetry.error('install', error);
            throw error;
        }
    }
    /**
     * Generates sys_claim records on the instance to mark desired records as moved into this global app
     *
     * @param sysIds Array of sysIds to move to the app
     */
    async moveToApp(options) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('move.to-app');
            const { sysIds } = options;
            const { scope, scopeId } = this.project.getConfig();
            if (scope !== 'global') {
                throw Error(`Only Global apps can customize files by moving them into the app`);
            }
            const connector = this.getConnector();
            const logger = this.project.getLogger();
            const scopeInfo = await connector.getScopeInfo(scope, scopeId);
            if (!scopeInfo) {
                throw Error(`App ${scopeId} must be present on the instance to move files. Run 'now-sdk install' to install app.`);
            }
            const claimed = await connector.moveToApp(sysIds, scopeId);
            if (claimed.length === 0) {
                logger.info(`Moved 0 files. Please ensure files are valid records`);
                return;
            }
            logger.info(`Successfully moved ${claimed.length} file(s) to app`);
            const claimedIds = claimed.map((claim) => claim.sysId);
            const failedToClaim = sysIds.filter((id) => !claimedIds.includes(id));
            if (failedToClaim && failedToClaim.length > 0) {
                logger.warn(`Failed to move:\n ${failedToClaim.join('\n\t')}`);
            }
            const result = await this.transform0({
                method: 'incremental',
            });
            this.project.save();
            this.telemetry.sendEvent(telemetryTimer.end({
                data: {
                    scopeId,
                },
            }));
            return result;
        }
        catch (error) {
            this.telemetry.error('move.to-app', error);
            throw error;
        }
    }
    async installConfigurations(options) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('install.configurations');
            const files = [];
            const { appOutputDir, scopeId } = this.project.getConfig();
            const outputDir = this.project.resolvePath(appOutputDir);
            (0, util_1.traverseDirectory)(this.project.getFileSystem(), outputDir, {
                extensions: ['.xml'],
                visitor: (file) => {
                    files.push(file);
                },
            });
            const updateSetId = await this.uploadXMLFiles(files, options?.targetUpdateSetId ? { targetUpdateSetId: options.targetUpdateSetId } : undefined);
            await this.runPostInstallTasks('configuration', options?.skipFlags);
            this.telemetry.sendEvent(telemetryTimer.end({
                data: {
                    scopeId,
                    installed: true,
                },
            }));
            return updateSetId;
        }
        catch (error) {
            this.telemetry.error('install.configurations', error);
            throw error;
        }
    }
    /**
     * Returns the last status of the app upgrade.
     */
    async installStatus() {
        const { scope } = this.project.getConfig();
        const connector = this.getConnector();
        return await connector.appUpgradeStatus(scope);
    }
    /**
     * Download the app contents from the connected ServiceNow instance.
     *
     * @param options Options for the download process, defaults to `{ method: 'complete' }`
     * @param downloadPath The path where the downloaded files will be saved. Defaults to `os.tmpdir()/servicenow/download/GUID()`.
     * @returns The path to the downloaded zip file.
     */
    async download(options = {
        method: 'complete',
    }, downloadPath = sdk_build_core_1.path.join(os.tmpdir(), 'servicenow/download', (0, sdk_build_core_1.GUID)())) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('download');
            const connector = this.getConnector();
            const { scope, scopeId } = this.project.getConfig();
            let result;
            const logger = this.project.getLogger();
            if (options.method === 'update-set') {
                logger.info(`downloading update set ${options.updateSetId}`);
                result = await connector.downloadUpdateSet(options.updateSetId, scopeId);
            }
            else {
                logger.info(`downloading package for ${scope} ${options.method}`);
                result = await connector.download(scopeId, options);
            }
            if (!result.ok || !result.body) {
                const body = await result.json();
                if (body && typeof body.result === 'string') {
                    throw new Error(body.result);
                }
                throw new Error(`Failed to download package for ${scope}`);
            }
            const fs = this.project.getFileSystem();
            let writePath;
            if (options.method === 'update-set') {
                writePath = sdk_build_core_1.path.join(downloadPath, `sys_remote_update_set.xml`);
            }
            else {
                writePath = sdk_build_core_1.path.join(downloadPath, `${scopeId}-${options.method}.zip`);
            }
            (0, util_1.createDirIfNotExists)(downloadPath, fs);
            await (0, util_1.writeFileStream)(fs, writePath, result.body);
            this.telemetry.sendEvent(telemetryTimer.end({
                data: {
                    scopeId,
                    completeAppDownload: options.method === 'complete',
                },
            }));
            return writePath;
        }
        catch (error) {
            this.telemetry.error('download', error);
            throw error;
        }
    }
    /**
     * Downloads and generates TypeScript type definitions for dependencies from a ServiceNow instance.
     *
     * This method can download two types of definitions:
     * - **Script definitions**: TypeScript definitions for ServiceNow APIs (glide.*.d.ts files)
     * - **Fluent types**: TypeScript definitions for configured dependencies from now.config.json
     *
     * @param options - Configuration options for what to download
     * @param options.downloadScripts - Download script type definitions (default: true)
     * @param options.downloadFluent - Download fluent type definitions from now.config.json (default: true)
     *
     * @example
     * ```typescript
     * // Download all dependencies (default)
     * await orchestrator.types()
     *
     * // Download only script definitions
     * await orchestrator.types({ downloadScripts: true, downloadFluent: false })
     *
     * // Download only fluent types
     * await orchestrator.types({ downloadScripts: false, downloadFluent: true })
     * ```
     *
     * @throws {Error} If fetching or generating types fails
     *
     * @see {@link addDependency} for adding new dependencies programmatically
     */
    async types(options) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('dependencies');
            const { downloadScripts = true, downloadFluent = true } = options || {};
            const logger = this.project.getLogger();
            const appDependencies = new dependencies_1.ApplicationDependencies(this.project, this.getConnector());
            if (downloadScripts) {
                logger.info('Processing script type definitions...');
                await appDependencies.addScriptDefinitions();
            }
            if (downloadFluent) {
                logger.info('Processing fluent type dependencies...');
                await appDependencies.addFluentTypes();
            }
            this.telemetry.sendEvent(telemetryTimer.end());
        }
        catch (error) {
            this.telemetry.error('dependencies', error);
            throw error;
        }
    }
    /**
     * Adds new dependency items to the project and updates now.config.json.
     *
     * This method:
     * 1. Updates now.config.json with the new dependency items
     * 2. Fetches the records from the ServiceNow instance
     * 3. Generates TypeScript type definitions
     * 4. Updates index files
     *
     * Supports adding dependencies from any ServiceNow table, with special handling for:
     * - **Flow items**: actions, triggers, subflows (stored in dependencies.<scope>.automation.*)
     * - **Tables**: Table definitions (stored in dependencies.<scope>.tables)
     * - **Custom records**: Any other table (stored in dependencies.<scope>.<table_name>)
     *
     * @param options - Configuration for the dependency to add
     * @param options.table - Table name or alias (e.g., 'actions', 'sys_security_acl', 'sys_user')
     * @param options.ids - Array of sys_ids or table names. Use ['*'] to fetch all records from a scope
     * @param options.scope - Scope filter (e.g., 'global', 'x_my_app'). Required when using wildcard '*'
     *
     * @example
     * ```typescript
     * // Add specific flow actions
     * await orchestrator.addDependency({
     *     table: 'actions',
     *     ids: ['action_id1', 'action_id2']
     * })
     *
     * // Add all actions from a scope using wildcard
     * await orchestrator.addDependency({
     *     table: 'actions',
     *     ids: ['*'],
     *     scope: 'global'
     * })
     *
     * // Add specific security ACLs
     * await orchestrator.addDependency({
     *     table: 'sys_security_acl',
     *     ids: ['acl_id1', 'acl_id2']
     * })
     *
     * // Add all ACLs from a custom scope
     * await orchestrator.addDependency({
     *     table: 'sys_security_acl',
     *     ids: ['*'],
     *     scope: 'x_my_app'
     * })
     *
     * // Add table definitions
     * await orchestrator.addDependency({
     *     table: 'tables',
     *     ids: ['incident', 'problem', 'change_request']
     * })
     * ```
     *
     * @throws {Error} If wildcard '*' is used without a scope
     * @throws {Error} If fetching records from the instance fails
     * @throws {Error} If updating now.config.json fails
     *
     * @see {@link types} for downloading existing dependencies
     */
    async addDependency(options) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('dependencies.add');
            const logger = this.project.getLogger();
            const appDependencies = new dependencies_1.ApplicationDependencies(this.project, this.getConnector());
            logger.info(`Adding ${options.table}: ${options.ids.join(', ')} (scope: ${options.scope})`);
            await appDependencies.addDependencyItems(options.table, options.ids, options.scope);
            logger.info(`Successfully added ${options.ids.length} ${options.table} records`);
            this.telemetry.sendEvent(telemetryTimer.end());
        }
        catch (error) {
            this.telemetry.error('dependencies.add', error);
            throw error;
        }
    }
    async run(options) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('run');
            const { scopeId } = this.project.getConfig();
            const scriptName = options.script;
            await (0, util_1.runScriptInProject)({
                scriptName,
                project: this.project,
                credential: this.credential,
                throwIfNotFound: true,
                args: options.args,
            });
            this.telemetry.sendEvent(telemetryTimer.end({
                data: { scopeId },
            }));
        }
        catch (error) {
            this.telemetry.error('run', error);
            throw error;
        }
    }
    /**
     * Applies a template to the project.
     * @param id The ID of the template to apply.
     * @returns The temlate files that were applied to the project
     */
    async applyTemplate(id, params) {
        try {
            const telemetryTimer = this.telemetry.startTimerEvent('init.apply-template');
            const config = this.project.getConfig();
            const template = project_factory_1.templates[id];
            if (!template) {
                throw new Error(`Template ${id} not found`);
            }
            const projectFactory = new project_factory_1.ProjectFactory(this.project.getFileSystem(), this.telemetry);
            const renderedTemplates = (0, project_factory_1.renderTemplates)(template.files, { ...config, ...params }, projectFactory.getOriginalFiles(this.project.getRootDir()));
            const fs = this.project.getFileSystem();
            const rootDir = this.project.getRootDir();
            for (const { path: filePath, content, templateFile } of renderedTemplates) {
                const absolutePath = sdk_build_core_1.path.join(rootDir, filePath);
                if (sdk_build_core_1.FileSystem.existsSync(fs, absolutePath)) {
                    const existingContent = fs.readFileSync(absolutePath, { encoding: 'utf-8' });
                    if ((0, project_factory_1.wouldTemplateConflict)(templateFile.content, existingContent, content)) {
                        throw new Error(`Collision detected with template file that is not mergable: ${filePath}`);
                    }
                }
                this.project.addFile({ path: filePath, content });
            }
            this.project.save();
            this.telemetry.sendEvent(telemetryTimer.end({
                data: { scopeId: config.scopeId, existing: true, templateId: id, ...params },
            }));
            return renderedTemplates;
        }
        catch (error) {
            this.telemetry.error('init.apply-template', error, {
                existing: true,
                ...{ templateId: id, ...params },
            });
            throw error;
        }
    }
    /**
     * @deprecated Diagnostics are logged automatically
     */
    logDiagnostics(_diagnostics, _options = { colors: true }) { }
}
exports.Orchestrator = Orchestrator;
//# sourceMappingURL=orchestrator.js.map