"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.cloneNode = cloneNode;
const node_util_1 = require("node:util");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_os_1 = require("node:os");
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:timers/promises");
const node_https_1 = require("node:https");
const node_http_1 = require("node:http");
const yaml_1 = __importDefault(require("yaml"));
const cbor_x_1 = require("cbor-x");
const environmentManager_js_1 = __importDefault(require("../core/utility/environment/environmentManager.js"));
const logger = __importStar(require("../core/utility/logging/harper_logger.js"));
const installation_js_1 = require("../core/utility/installation.js");
const configUtils_js_1 = require("../core/config/configUtils.js");
const assignCmdEnvVariables_js_1 = __importDefault(require("../core/utility/assignCmdEnvVariables.js"));
const hdbTerms_ts_1 = require("../core/utility/hdbTerms.js");
/**
 * Environment Variables:
 *
 * Clone Methods:
 * This script supports two cloning approaches:
 * 1. Certificate-based: Uses pre-setup certs for authentication over WebSocket connections
 * 2. username/password or JWT token cloning: Uses credentials (username/password or token) with fetch calls between nodes
 *
 * Required (if not via CLI):
 * - HDB_LEADER_URL: URL of the leader node to clone from
 *
 * Required for username/password or JWT token cloning:
 * - HDB_LEADER_USERNAME + HDB_LEADER_PASSWORD: Admin credentials for username/password authentication
 * - HDB_LEADER_TOKEN: JWT authentication token for the leader node
 *
 * Optional:
 * - CLONE_SSH_KEYS: Clone SSH keys from leader (default: true)
 * - CLONE_JWT_KEYS: Clone JWT keys from leader (default: true)
 * - ALLOW_SELF_SIGNED: Allow self-signed certificates to be used for authentication (default: false)
 * - CLONE_NODE_UPDATE_STATUS: Update node status during clone (default: false)
 * - CLONE_SYNC_TIMEOUT: Sync timeout in milliseconds (default: 30000)
 * - REPLICATION_PORT: Port for replication
 * - FORCE_CLONE: Force clone even if node exists (default: false)
 * - ROOTPATH: HarperDB installation root path
 * - NODE_HOSTNAME: Hostname for this node
 *
 * CLI Arguments:
 * Boolean flags are presence-based: include the flag to enable, omit to disable.
 * --leader-url: URL of the leader node to clone from
 * --leader-username: Admin username for credential-based authentication
 * --leader-password: Admin password for credential-based authentication
 * --rootpath: HarperDB installation root path
 * --node-hostname: Hostname for this node
 * --replication-port: Port for replication
 * --skip-sync-monitor: Skip monitoring sync status (default: false)
 * --sync-timeout: Sync timeout in milliseconds (default: 30000)
 * --skip-ssh-keys: Skip cloning SSH keys (default: false)
 * --skip-jwt-keys: Skip cloning JWT keys (default: false)
 * --force-clone: Force clone even if node exists (default: false)
 * --allow-self-signed: Allow self-signed certificates (default: false)
 */
const DEFAULT_SYNC_TIMEOUT_MS = 300000;
const DEFAULT_SYNC_CHECK_INTERVAL_MS = 3000;
const DEFAULT_REPLICATION_PORT = '9933';
const CONFIG_TO_EXCLUDE_FROM_CLONE = {
    clustering_nodename: true,
    clustering_leafserver_streams_path: true,
    clustering_tls_certificate: true,
    clustering_tls_privatekey: true,
    clustering_tls_certificateauthority: true,
    logging_file: true,
    logging_root: true,
    logging_rotation_path: true,
    operationsapi_network_domainsocket: true,
    operationsapi_tls_certificate: true,
    operationsapi_tls_privatekey: true,
    operationsapi_tls_certificateauthority: true,
    rootpath: true,
    storage_path: true,
    storage_audit_path: true,
    databases: true,
    mqtt_network_mtls_certificateauthority: true,
    componentsroot: true,
    tls_certificate: true,
    tls_privatekey: true,
    tls_certificateauthority: true,
    replication_hostname: true,
    replication_url: true,
    cloned: true,
    node_hostname: true,
};
const { values } = (0, node_util_1.parseArgs)({
    options: {
        'leader-url': { type: 'string' },
        'leader-username': { type: 'string' },
        'leader-password': { type: 'string' },
        'leader-token': { type: 'string' },
        'rootpath': { type: 'string' },
        'node-hostname': { type: 'string' },
        'replication-port': { type: 'string' },
        'skip-sync-monitor': { type: 'boolean' },
        'sync-timeout': { type: 'string' },
        'skip-ssh-keys': { type: 'boolean' },
        'skip-jwt-keys': { type: 'boolean' },
        'force-clone': { type: 'boolean' },
        'allow-self-signed': { type: 'boolean' },
    },
    strict: false,
});
const leaderURL = values['leader-url'] || process.env.HDB_LEADER_URL;
const leaderUsername = values['leader-username'] || process.env.HDB_LEADER_USERNAME;
const leaderPassword = values['leader-password'] || process.env.HDB_LEADER_PASSWORD;
const leaderToken = values['leader-token'] || process.env.HDB_LEADER_TOKEN;
const skipSyncMonitor = values['skip-sync-monitor'] ?? process.env.CLONE_SKIP_SYNC_MONITOR === 'true';
const syncTimeoutMs = Math.max(1, parseInt(values['sync-timeout'] || process.env.CLONE_SYNC_TIMEOUT, 10) || DEFAULT_SYNC_TIMEOUT_MS);
const replicationPort = values['replication-port'] || process.env.REPLICATION_PORT;
const skipSSHKeys = values['skip-ssh-keys'] ?? process.env.CLONE_SKIP_SSH_KEYS === 'true';
const skipJWTKeys = values['skip-jwt-keys'] ?? process.env.CLONE_SKIP_JWT_KEYS === 'true';
const forceClone = values['force-clone'] ?? process.env.FORCE_CLONE === 'true';
const allowSelfSigned = values['allow-self-signed'] ?? process.env.ALLOW_SELF_SIGNED === 'true';
const nodeHostname = values['node-hostname'] || process.env.NODE_HOSTNAME || process.env.REPLICATION_HOSTNAME;
let rootPath = values['rootpath'] || values['ROOTPATH'] || process.env.ROOTPATH;
const usingCertAuth = !(leaderUsername && leaderPassword) && !leaderToken;
let harperLogger;
let leaderReplicationURL;
let hdbConfig = {};
let freshClone = false;
async function cloneNode() {
    // Clone using websockets with certificate-based auth, or with credential/token auth if provided
    log(`Starting clone node from leader: ${leaderURL} using ${usingCertAuth ? 'certificate' : 'credential'} auth`);
    // If a root path was provided, use it. Otherwise, check for existing install to get root path or start a fresh clone and generate a new root path.
    resolveRootPath();
    // Make sure rootPath is set in all the places that is could be needed.
    // This is especially important for local testing where multiple Harper versions are running on the same machine.
    environmentManager_js_1.default.setHdbBasePath(rootPath);
    leaderReplicationURL = leaderURL
        .replace('http://', 'ws://')
        .replace('https://', 'wss://')
        .replace(/:(\d+)/, `:${replicationPort || DEFAULT_REPLICATION_PORT}`);
    // Check to see if there is an existing config file to read additional config from
    const cfgPath = (0, node_path_1.join)(rootPath, hdbTerms_ts_1.HARPER_CONFIG_FILE);
    const oldCfgPath = (0, node_path_1.join)(rootPath, hdbTerms_ts_1.HDB_CONFIG_FILE);
    let harperConfigPath;
    if (pathExists(cfgPath)) {
        harperConfigPath = cfgPath;
        log(`Existing config file found at ${cfgPath}, reading config from this file`);
    }
    else if (pathExists(oldCfgPath)) {
        harperConfigPath = oldCfgPath;
        log(`Existing config file found at ${oldCfgPath}, reading config from this file`);
    }
    else {
        log('No existing config file found, starting with empty config');
    }
    if (harperConfigPath) {
        try {
            const yamlContent = (0, node_fs_1.readFileSync)(harperConfigPath, 'utf8');
            const hdbConfigJson = yaml_1.default.parse(yamlContent);
            hdbConfig = (0, configUtils_js_1.flattenConfig)(hdbConfigJson);
        }
        catch (err) {
            log(`Error reading existing config file ${harperConfigPath} on clone: ${err}`, 'error');
        }
    }
    // If not a fresh clone, and the existing config shows the instance is already cloned, skip clone process and start normally
    if (hdbConfig?.cloned && !forceClone) {
        log('Skipping clone, instance already marked as cloned. Starting Harper.');
        environmentManager_js_1.default.initSync();
        const { main } = await import('../core/bin/run.js');
        return main();
    }
    if (!usingCertAuth) {
        // Request to leader to verify connectivity and credentials before proceeding with clone
        // Cannot check if cloning with WS - module initialization order prevents access to required variables
        await leaderRequest({ operation: hdbTerms_ts_1.OPERATIONS_ENUM.GET_STATUS });
    }
    // Install Harper if this is a fresh clone or if the system database does not exist
    const systemDBPath = getDBPath(hdbTerms_ts_1.SYSTEM_SCHEMA_NAME);
    const systemExists = pathExists(systemDBPath);
    if (freshClone || !systemExists) {
        await installHarper();
    }
    // Start Harper to prepare for clone operations
    const { main } = await import('../core/bin/run.js');
    await main();
    logger.initLogSettings();
    harperLogger = logger.loggerWithTag('cloneNode');
    // Get the config from the leader and write it to the existing local config file, excluding any parameters that should not be cloned
    await cloneConfig();
    // Clone applications that are deployed on the leader but not referenced in harper-config
    await cloneApplications();
    const setNodeRequest = {
        operation: hdbTerms_ts_1.OPERATIONS_ENUM.ADD_NODE,
        verify_tls: false, // set node cross-signs the cluster with harper self-signed certs
        url: leaderReplicationURL,
    };
    if (!usingCertAuth) {
        // If cloning using credential/token auth, we need to include the leader credentials in the set node request so that the leader can authenticate this node
        if (leaderToken) {
            setNodeRequest.authorization = 'Bearer ' + leaderToken;
        }
        else {
            setNodeRequest.authorization = {
                username: leaderUsername,
                password: leaderPassword,
            };
        }
    }
    // Restarting workers to ensure new configuration it loaded.
    log('Restarting workers to apply new configuration');
    const { restartWorkers } = await import('../core/server/threads/manageThreads.js');
    await restartWorkers();
    // Dynamically importing setNode because it was causing early usage of rootpath var install before it was initialized.
    const { setNode } = await import('../replication/setNode.js');
    // Set node will set up replication between this node and the leader,
    // which will trigger the sync of data including some system tables like users and roles.
    log('Sending set node request to leader to establish replication and trigger data sync');
    const setNodeResponse = await setNode(setNodeRequest);
    log(`Response from set node: ${setNodeResponse}`);
    await cloneJWTKeys();
    await cloneSSHKeys();
    // Monitor the synchronization status of the databases after cloning and update availability status once sync is complete
    await monitorSync();
    // Delete clone-temp-admin only after monitorSync() so that the account remains valid while
    // the leader establishes replication and syncs real users. Deleting it earlier leaves the
    // node with no users during setNode(), which prevents replication from being established.
    // Runs on retry too (when systemExists but cloned not yet set) via !hdbConfig?.cloned.
    if ((usingCertAuth || leaderToken) && (!systemExists || !hdbConfig?.cloned)) {
        try {
            const { databases } = await import('../core/resources/databases.js');
            await databases.system.hdb_user.delete({ username: 'clone-temp-admin' });
        }
        catch (err) {
            log(`Warning: failed to delete clone-temp-admin: ${err}`, 'error');
        }
    }
    // Set a config value to indicate that this node has been cloned, which can be used by other processes to check clone status and prevent duplicate cloning
    (0, configUtils_js_1.updateConfigValue)(hdbTerms_ts_1.CONFIG_PARAMS.CLONED, true);
    log(`Clone from leader node ${leaderURL} complete`);
}
/**
 * Monitors database synchronization status after cloning and updates availability status once complete.
 * Polls at regular intervals until sync completes or timeout is reached.
 */
async function monitorSync() {
    if (skipSyncMonitor)
        return;
    const { clusterStatus } = await import('../replication/clusterStatus.js');
    const { set: setStatus } = await import('../core/server/status/index.js');
    // Get last updated record timestamps for all DB and write to file
    // These values can be used for checking when the clone replication has caught up with the leader
    const targetTimestamps = await getLastUpdatedRecord();
    if (!targetTimestamps || Object.keys(targetTimestamps).length === 0) {
        log('No target timestamps available to check synchronization status', 'error');
        return;
    }
    log(`Starting to monitor sync status. Will check every ${DEFAULT_SYNC_CHECK_INTERVAL_MS}ms for up to ${Math.round(syncTimeoutMs / 60000)} minutes`);
    const timeoutAt = Date.now() + syncTimeoutMs;
    let syncComplete = false;
    let loopCount = 0;
    while (!syncComplete && Date.now() < timeoutAt) {
        try {
            syncComplete = await checkSyncStatus(targetTimestamps, clusterStatus);
            if (syncComplete) {
                log('All databases synchronized');
                try {
                    await setStatus({ id: 'availability', status: 'Available' });
                }
                catch (err) {
                    // Don't fail sync monitoring due to status update failure
                    log(`Failed to update availability status: ${err}`, 'error');
                }
                break; // Exit loop on successful sync
            }
            // Log every other iteration to reduce noise
            if (loopCount % 2 === 0) {
                log(`Sync incomplete, retrying in ${DEFAULT_SYNC_CHECK_INTERVAL_MS}ms`);
            }
            loopCount++;
            await (0, promises_1.setTimeout)(DEFAULT_SYNC_CHECK_INTERVAL_MS);
        }
        catch (err) {
            log(`Error checking sync status: ${err}`, 'error');
            await (0, promises_1.setTimeout)(DEFAULT_SYNC_CHECK_INTERVAL_MS); // Still wait on error
        }
    }
}
/**
 * Check if all databases are synchronized by comparing timestamps
 * Compares the most recent timestamp in each local database against the target timestamps from the leader
 * @param {Object} targetTimestamps - Target timestamps to check against
 * @param clusterStatus - Function to get the current cluster status, which includes replication status and timestamps for each database connection
 * @returns {Promise<boolean>} - True if all databases are synchronized
 */
async function checkSyncStatus(targetTimestamps, clusterStatus) {
    const clusterResponse = await clusterStatus();
    log(`clone sync check cluster status response: ${clusterResponse}`, 'debug');
    if (!clusterResponse) {
        log('No cluster status response received for clone, will wait and retry');
        return false;
    }
    if (!clusterResponse.connections?.length) {
        log('No connections found in cluster status response for clone, will wait and retry');
        return false;
    }
    // Find the leader replication connection
    const leaderConnection = clusterResponse.connections.find((conn) => conn.url === leaderReplicationURL);
    if (!leaderConnection) {
        log('No connection found matching leader replication URL, will wait and retry');
        return false;
    }
    if (!leaderConnection.database_sockets?.length) {
        log(`No database sockets found for connection leader ${leaderConnection.name}`, 'debug');
        return false;
    }
    // Check sync status for each database socket
    for (const socket of leaderConnection.database_sockets) {
        const dbName = socket.database;
        const targetTime = targetTimestamps[dbName];
        // Skip if no target time for this database
        if (!targetTime) {
            log(`Database ${dbName}: No target timestamp, skipping sync check`, 'debug');
            continue;
        }
        // Raw version timestamp from RECEIVED_VERSION_POSITION (high-precision float64)
        // This preserves sub-millisecond precision needed for accurate sync comparison
        const receivedVersion = socket.lastReceivedVersion;
        if (!receivedVersion) {
            log(`No lastReceivedVersion data received yet for database ${dbName}`, 'debug');
            return false;
        }
        if (receivedVersion < targetTime) {
            log(`Database ${dbName}: Not yet synchronized (received: ${receivedVersion}, target: ${targetTime}, gap: ${targetTime - receivedVersion}ms)`);
            return false;
        }
        log(`Database ${dbName}: Synchronized`, 'debug');
    }
    return true;
}
/**
 * Will loop through a system describe and a describeAll to compare the last updated record for each table
 * and record the most recent timestamp for each database in a JSON file.
 * @returns {Promise<void>}
 */
async function getLastUpdatedRecord() {
    log('Getting last updated record timestamp for all database', 'debug');
    const lastUpdated = {};
    const systemDb = await leaderRequest({ operation: 'describe_database', database: 'system' });
    lastUpdated['system'] = findMostRecentTimestamp(systemDb);
    const allDb = await leaderRequest({ operation: 'describe_all' });
    for (const db in allDb) {
        // requestId is part of the describe response so we ignore it
        if (typeof allDb[db] !== 'object')
            continue;
        lastUpdated[db] = findMostRecentTimestamp(allDb[db]);
    }
    const lastUpdatedFilePath = (0, node_path_1.join)(rootPath, 'tmp', 'lastUpdated.json');
    log(`Writing last updated database timestamps to: ${lastUpdatedFilePath}`, 'debug');
    writeJsonSync(lastUpdatedFilePath, lastUpdated);
    return lastUpdated;
}
/**
 * Find the most recent last_updated_record timestamp across all tables in a database
 * @param {Object} dbObj - Database object or describe response containing tables
 * @returns {number} - Most recent timestamp, or 0 if none found
 */
function findMostRecentTimestamp(dbObj) {
    let mostRecent = 0;
    for (const table in dbObj) {
        const tableObj = dbObj[table];
        // requestId is part of the describe response so we ignore it
        if (typeof tableObj !== 'object' || tableObj == null)
            continue;
        if (tableObj.last_updated_record > mostRecent) {
            mostRecent = tableObj.last_updated_record;
        }
    }
    return mostRecent;
}
/**
 * Clones SSH keys from the leader node by requesting a list of keys and then fetching each key's data to add to this node.
 */
async function cloneSSHKeys() {
    if (skipSSHKeys)
        return;
    const { addSSHKey } = await import('../security/sshKeyOperations.js');
    try {
        const keys = await leaderRequest({ operation: 'list_ssh_keys' });
        if (!keys?.length) {
            log('No SSH keys found on leader node to clone');
            return;
        }
        for (const keyName of keys) {
            log('Cloning SSH key:', keyName.name);
            const keyData = await leaderRequest({
                operation: 'get_ssh_key',
                name: keyName.name,
            });
            await addSSHKey(keyData);
        }
    }
    catch (err) {
        log(`Error cloning SSH keys: ${err}`, 'error');
    }
}
/**
 * Clones JWT keys from the leader node by requesting each key's data and writing it to the local file system.
 */
async function cloneJWTKeys() {
    if (skipJWTKeys)
        return;
    try {
        log('Cloning JWT keys');
        const keysDir = (0, node_path_1.join)(rootPath, hdbTerms_ts_1.LICENSE_KEY_DIR_NAME);
        const jwtPublic = await leaderRequest({
            operation: 'get_key',
            name: '.jwtPublic',
        });
        (0, node_fs_1.writeFileSync)((0, node_path_1.join)(keysDir, hdbTerms_ts_1.JWT_ENUM.JWT_PUBLIC_KEY_NAME), jwtPublic.message);
        const jwtPrivate = await leaderRequest({
            operation: 'get_key',
            name: '.jwtPrivate',
        });
        (0, node_fs_1.writeFileSync)((0, node_path_1.join)(keysDir, hdbTerms_ts_1.JWT_ENUM.JWT_PRIVATE_KEY_NAME), jwtPrivate.message);
        const jwtPass = await leaderRequest({
            operation: 'get_key',
            name: '.jwtPass',
        });
        (0, node_fs_1.writeFileSync)((0, node_path_1.join)(keysDir, hdbTerms_ts_1.JWT_ENUM.JWT_PASSPHRASE_NAME), jwtPass.message);
    }
    catch (err) {
        log(`Error cloning JWT keys: ${err}`, 'error');
    }
}
async function cloneConfig() {
    log('Cloning config from leader');
    const leaderConfigData = await leaderRequest({ operation: hdbTerms_ts_1.OPERATIONS_ENUM.GET_CONFIGURATION });
    if (!leaderConfigData.hasOwnProperty('rootPath')) {
        throw new Error('Leader config data check failed: rootPath is missing from leader config response');
    }
    // Build base configuration with node-specific settings
    const configData = {
        rootpath: rootPath,
        node_hostname: nodeHostname,
    };
    if (replicationPort) {
        configData[hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_PORT] = replicationPort;
    }
    const flattenedLeaderConfig = (0, configUtils_js_1.flattenConfig)(leaderConfigData);
    for (const [param, value] of Object.entries(flattenedLeaderConfig)) {
        // Skip objects (except arrays) and explicitly excluded parameters
        const isNonArrayObject = value !== null && typeof value === 'object' && !Array.isArray(value);
        if (isNonArrayObject || CONFIG_TO_EXCLUDE_FROM_CLONE[param]) {
            continue;
        }
        // Skip component packages/ports that reference local leader paths
        if ((param.includes('_package') || param.includes('_port')) && value?.includes?.('hdb/components')) {
            continue;
        }
        // Only add config if not already present in local config
        if (!hdbConfig[param]) {
            configData[param] = flattenedLeaderConfig[param];
        }
    }
    // Override with local config values (excluding nested objects, but including databases array)
    for (const [param, value] of Object.entries(hdbConfig)) {
        const isNonArrayObject = param !== 'databases' && typeof value === 'object' && !Array.isArray(value);
        if (isNonArrayObject) {
            continue;
        }
        configData[param] = value;
    }
    // Apply command-line and environment variable overrides
    const cliArgs = (0, assignCmdEnvVariables_js_1.default)(Object.keys(hdbTerms_ts_1.CONFIG_PARAM_MAP), true);
    Object.assign(configData, cliArgs);
    // Write final configuration to file
    (0, configUtils_js_1.createConfigFile)(configData, true);
}
/**
 * Clones applications from the leader that are deployed to the applications directory but not referenced in harper-config.
 * Applications referenced in harper-config are handled by normal config cloning and reinstalled on startup.
 */
async function cloneApplications() {
    log('Cloning filesystem-only applications from leader');
    let applicationsResponse;
    try {
        applicationsResponse = await leaderRequest({ operation: hdbTerms_ts_1.OPERATIONS_ENUM.GET_COMPONENTS });
    }
    catch (err) {
        log(`Failed to get applications from leader: ${err}`, 'error');
        return;
    }
    const entries = applicationsResponse?.entries ?? [];
    // filesystem-only applications will not have a `package` property
    const filesystemOnlyApplications = entries.filter((entry) => Array.isArray(entry.entries) && !entry.package);
    if (filesystemOnlyApplications.length === 0) {
        log('No filesystem-only applications found on leader to clone');
        return;
    }
    log(`Cloning ${filesystemOnlyApplications.length} application(s) not referenced in harper-config`);
    const { Application, prepareApplication } = await import("../core/components/Application.js");
    for (const entry of filesystemOnlyApplications) {
        const applicationName = entry.name;
        log(`Cloning application: ${applicationName}`);
        try {
            const packageResponse = await leaderRequest({
                operation: hdbTerms_ts_1.OPERATIONS_ENUM.PACKAGE_COMPONENT,
                project: applicationName,
                skip_node_modules: true,
            });
            const application = new Application({
                name: applicationName,
                payload: packageResponse.payload,
            });
            await prepareApplication(application);
            log(`Successfully cloned application: ${applicationName}`);
        }
        catch (err) {
            log(`Failed to clone application '${applicationName}': ${err}`, 'error');
        }
    }
}
/**
 * Send a request to the leader node for the given operation, using either WebSockets or HTTP based on the useWS flag
 * @param operation
 */
async function leaderRequest(operation) {
    if (usingCertAuth) {
        // Dynamically importing the replicator module because it was causing early import of rootpath var on
        // install before it was initialized.
        const { sendOperationToNode } = await import('../replication/replicator.js');
        return sendOperationToNode({ url: leaderReplicationURL }, operation, {
            rejectUnauthorized: !allowSelfSigned,
        });
    }
    const headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/cbor',
    };
    if (leaderToken) {
        headers.Authorization = `Bearer ${leaderToken}`;
    }
    else {
        headers.Authorization = `Basic ${Buffer.from(`${leaderUsername}:${leaderPassword}`).toString('base64')}`;
    }
    const body = JSON.stringify(operation);
    const url = new URL(leaderURL);
    const isHttps = url.protocol === 'https:';
    const port = url.port ? parseInt(url.port, 10) : isHttps ? 443 : 80;
    const path = url.pathname + url.search;
    const requestHeaders = { ...headers, 'Content-Length': Buffer.byteLength(body) };
    const { statusCode, statusMessage, contentType, responseBody } = await new Promise((resolve, reject) => {
        const callback = (res) => {
            const chunks = [];
            res.on('data', (chunk) => chunks.push(chunk));
            res.on('end', () => resolve({
                statusCode: res.statusCode,
                statusMessage: res.statusMessage,
                contentType: res.headers['content-type'] ?? '',
                responseBody: Buffer.concat(chunks),
            }));
            res.on('error', reject);
        };
        const req = isHttps
            ? (0, node_https_1.request)({
                hostname: url.hostname,
                port,
                path,
                method: 'POST',
                headers: requestHeaders,
                rejectUnauthorized: !allowSelfSigned,
            }, callback)
            : (0, node_http_1.request)({ hostname: url.hostname, port, path, method: 'POST', headers: requestHeaders }, callback);
        req.on('error', reject);
        req.write(body);
        req.end();
    });
    if (statusCode < 200 || statusCode >= 300) {
        throw new Error(`Leader request failed: ${statusCode} ${statusMessage}`);
    }
    if (contentType.includes('application/cbor')) {
        return (0, cbor_x_1.decode)(responseBody);
    }
    return JSON.parse(responseBody.toString('utf8'));
}
function log(message, level = 'notify') {
    const isError = level === 'error';
    const isDebug = level === 'debug';
    if (harperLogger) {
        if (isError)
            harperLogger.error?.(message);
        else if (isDebug) {
            harperLogger.debug?.(message);
            return;
        }
        else
            harperLogger.notify?.(message);
    }
    if (isError)
        console.error(message);
    else
        console.log(message);
}
/**
 * Installs Harper as the base for the clone operation
 */
async function installHarper() {
    log(`Clone installing Harper at root path: ${rootPath}`);
    if (usingCertAuth || leaderToken) {
        // Set temporary admin credentials if cloning without username/password to allow installation to complete.
        // These values will be replaced during the clone process after syncing system tables from the leader.
        process.env.HDB_ADMIN_USERNAME = 'clone-temp-admin';
        process.env.HDB_ADMIN_PASSWORD = (0, node_crypto_1.randomBytes)(20).toString('base64').slice(0, 10);
    }
    else {
        // Use provided leader credentials for this installation
        process.env.HDB_ADMIN_USERNAME = leaderUsername;
        process.env.HDB_ADMIN_PASSWORD = leaderPassword;
    }
    process.env.NODE_HOSTNAME = nodeHostname;
    process.env.TC_AGREEMENT = 'yes';
    process.env.ROOTPATH = rootPath;
    const { install, setIgnoreExisting } = await import('../core/utility/install/installer.js');
    setIgnoreExisting(true);
    await install();
}
/**
 * Check if a file or directory exists at the given path
 * @param path
 */
function pathExists(path) {
    try {
        (0, node_fs_1.accessSync)(path);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Write the given data as JSON to a file at the specified path
 * @param path
 * @param data
 */
function writeJsonSync(path, data) {
    try {
        // Create directory if it doesn't exist
        (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
        (0, node_fs_1.writeFileSync)(path, JSON.stringify(data, null, 2), 'utf8');
    }
    catch (err) {
        log(`Error writing JSON to ${path}: ${err}`, 'error');
    }
}
/**
 * Get the database path for a given database name
 * Checks to see if there is any custom DB pathing else uses the default storage path
 * @param dbName
 */
function getDBPath(dbName) {
    const dbConfig = environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES)?.[dbName];
    return dbConfig?.path || environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PATH) || (0, node_path_1.join)(rootPath, hdbTerms_ts_1.DATABASES_DIR_NAME);
}
/**
 * Determines the root path for the Harper installation based on the following order of precedence:
 * 1. ROOTPATH provided via CLI or environment variable
 * 2. Existing Harper installation root path (if found)
 * 3. Default root path at ~/harperdb
 * If an existing root path is found but does not exist on the filesystem, it will fall back to a fresh clone with a new root path.
 */
function resolveRootPath() {
    if (rootPath) {
        if (pathExists(rootPath)) {
            log(`Using root path: ${rootPath}`);
        }
        else {
            log(`Root path ${rootPath} does not exist, starting fresh clone`);
            freshClone = true;
        }
    }
    else if ((0, installation_js_1.isHdbInstalled)(environmentManager_js_1.default, logger)) {
        log('Existing Harper install found, getting default root path from config');
        try {
            // getConfiguration will get the config file name from the boot properties file and then read the harperdb-config file
            const config = (0, configUtils_js_1.getConfiguration)();
            rootPath = config[hdbTerms_ts_1.CONFIG_PARAMS.ROOTPATH];
        }
        catch (err) {
            throw new Error(`There was an error setting the clone default root path. Please set ROOTPATH using an environment or CLI variable. ${err}`);
        }
    }
    else {
        log('No Harper install found, starting fresh clone');
        freshClone = true;
        rootPath = (0, node_path_1.join)((0, node_os_1.homedir)(), hdbTerms_ts_1.HDB_ROOT_DIR_NAME);
        log(`Using default root path: ${rootPath}`);
    }
}
//# sourceMappingURL=cloneNode.js.map