// SPDX-License-Identifier: Apache-2.0
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import util from 'node:util';
import { SoloError } from './errors/solo-error.js';
import * as semver from 'semver';
import { Templates } from './templates.js';
import * as constants from './constants.js';
import { PrivateKey, ServiceEndpoint } from '@hiero-ledger/sdk';
import chalk from 'chalk';
import { PathEx } from '../business/utils/path-ex.js';
import { Flags as flags } from '../commands/flags.js';
import { execSync } from 'node:child_process';
import yaml from 'yaml';
export function getInternalAddress(releaseVersion, namespaceName, nodeAlias) {
    // @ts-expect-error TS2353: Object literal may only specify known properties
    return semver.gte(releaseVersion, '0.58.5', { includePrerelease: true })
        ? '127.0.0.1'
        : Templates.renderFullyQualifiedNetworkPodName(namespaceName, nodeAlias);
}
export function sleep(duration) {
    return new Promise((resolve) => {
        setTimeout(resolve, duration.toMillis());
    });
}
export function parseNodeAliases(input, consensusNodes, configManager) {
    let nodeAliases = splitFlagInput(input, ',');
    if (nodeAliases.length === 0) {
        nodeAliases = consensusNodes?.map((node) => {
            return node.name;
        });
        configManager?.setFlag(flags.nodeAliasesUnparsed, nodeAliases.join(','));
        if (!nodeAliases || nodeAliases.length === 0) {
            return [];
        }
    }
    return nodeAliases;
}
export function splitFlagInput(input, separator = ',') {
    if (!input) {
        return [];
    }
    else if (typeof input !== 'string') {
        throw new SoloError(`input [input='${input}'] is not a comma separated string`);
    }
    return input
        .split(separator)
        .map(s => s.trim())
        .filter(Boolean);
}
/**
 * @param arr - The array to be cloned
 * @returns a new array with the same elements as the input array
 */
export function cloneArray(array) {
    return structuredClone(array);
}
export function getTemporaryDirectory() {
    return fs.mkdtempSync(PathEx.join(os.tmpdir(), 'solo-'));
}
export function createBackupDirectory(destinationDirectory, prefix = 'backup', currentDate = new Date()) {
    const dateDirectory = util.format('%s%s%s_%s%s%s', currentDate.getFullYear(), currentDate.getMonth().toString().padStart(2, '0'), currentDate.getDate().toString().padStart(2, '0'), currentDate.getHours().toString().padStart(2, '0'), currentDate.getMinutes().toString().padStart(2, '0'), currentDate.getSeconds().toString().padStart(2, '0'));
    const backupDirectory = PathEx.join(destinationDirectory, prefix, dateDirectory);
    if (!fs.existsSync(backupDirectory)) {
        fs.mkdirSync(backupDirectory, { recursive: true });
    }
    return backupDirectory;
}
export function makeBackup(fileMap = new Map(), removeOld = true) {
    for (const entry of fileMap) {
        const sourcePath = entry[0];
        const destinationPath = entry[1];
        if (fs.existsSync(sourcePath)) {
            fs.cpSync(sourcePath, destinationPath);
            if (removeOld) {
                fs.rmSync(sourcePath);
            }
        }
    }
}
export function backupOldTlsKeys(nodeAliases, keysDirectory, currentDate = new Date(), directoryPrefix = 'tls') {
    const backupDirectory = createBackupDirectory(keysDirectory, `unused-${directoryPrefix}`, currentDate);
    const fileMap = new Map();
    for (const nodeAlias of nodeAliases) {
        const sourcePath = PathEx.join(keysDirectory, Templates.renderTLSPemPrivateKeyFile(nodeAlias));
        const destinationPath = PathEx.join(backupDirectory, Templates.renderTLSPemPrivateKeyFile(nodeAlias));
        fileMap.set(sourcePath, destinationPath);
    }
    makeBackup(fileMap, true);
    return backupDirectory;
}
export function backupOldPemKeys(nodeAliases, keysDirectory, currentDate = new Date(), directoryPrefix = 'gossip-pem') {
    const backupDirectory = createBackupDirectory(keysDirectory, `unused-${directoryPrefix}`, currentDate);
    const fileMap = new Map();
    for (const nodeAlias of nodeAliases) {
        const sourcePath = PathEx.join(keysDirectory, Templates.renderGossipPemPrivateKeyFile(nodeAlias));
        const destinationPath = PathEx.join(backupDirectory, Templates.renderGossipPemPrivateKeyFile(nodeAlias));
        fileMap.set(sourcePath, destinationPath);
    }
    makeBackup(fileMap, true);
    return backupDirectory;
}
export function isNumeric(string_) {
    if (typeof string_ !== 'string') {
        return false;
    } // we only process strings!
    return (!Number.isNaN(Number.parseInt(string_)) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
        !Number.isNaN(Number.parseFloat(string_))); // ...and ensure strings of whitespace fail
}
export function getEnvironmentValue(environmentVariableArray, name) {
    const kvPair = environmentVariableArray.find(v => v.startsWith(`${name}=`));
    return kvPair ? kvPair.split('=')[1] : null;
}
export function parseIpAddressToUint8Array(ipAddress) {
    const parts = ipAddress.split('.');
    const uint8Array = new Uint8Array(4);
    for (let index = 0; index < 4; index++) {
        uint8Array[index] = Number.parseInt(parts[index], 10);
    }
    return uint8Array;
}
/** If the basename of the src did not match expected basename, rename it first, then copy to destination */
export function renameAndCopyFile(sourceFilePath, expectedBaseName, destinationDirectory, logger) {
    const sourceDirectory = path.dirname(sourceFilePath);
    if (path.basename(sourceFilePath) !== expectedBaseName) {
        fs.renameSync(sourceFilePath, PathEx.join(sourceDirectory, expectedBaseName));
    }
    // copy public key and private key to key directory
    fs.copyFile(PathEx.joinWithRealPath(sourceDirectory, expectedBaseName), PathEx.join(destinationDirectory, expectedBaseName), error => {
        if (error) {
            throw new SoloError(`Error copying file: ${error.message}`);
        }
    });
}
/**
 * Add debug options to valuesArg used by helm chart
 * @param valuesArg the valuesArg to update
 * @param debugNodeAlias the node ID to attach the debugger to
 * @param index the index of extraEnv to add the debug options to
 * @returns updated valuesArg
 */
export function addDebugOptions(valuesArgument, debugNodeAlias, index = 0) {
    if (debugNodeAlias) {
        const nodeId = Templates.nodeIdFromNodeAlias(debugNodeAlias);
        valuesArgument += ` --set "hedera.nodes[${nodeId}].root.extraEnv[${index}].name=JAVA_OPTS"`;
        valuesArgument += String.raw ` --set "hedera.nodes[${nodeId}].root.extraEnv[${index}].value=-agentlib:jdwp=transport=dt_socket\,server=y\,suspend=y\,address=*:${constants.JVM_DEBUG_PORT}"`;
    }
    return valuesArgument;
}
/**
 * Returns an object that can be written to a file without data loss.
 * Contains fields needed for adding a new node through separate commands
 * @param ctx
 * @returns file writable object
 */
export function addSaveContextParser(context_) {
    const exportedContext = {};
    const config = context_.config;
    const exportedFields = ['tlsCertHash', 'upgradeZipHash', 'newNode'];
    exportedContext.signingCertDer = context_.signingCertDer.toString();
    exportedContext.gossipEndpoints = context_.gossipEndpoints.map((ep) => `${ep.getDomainName}:${ep.getPort}`);
    exportedContext.grpcServiceEndpoints = context_.grpcServiceEndpoints.map((ep) => `${ep.getDomainName}:${ep.getPort}`);
    exportedContext.adminKey = context_.adminKey.toString();
    // @ts-ignore
    exportedContext.existingNodeAliases = config.existingNodeAliases;
    for (const property of exportedFields) {
        exportedContext[property] = context_[property];
    }
    return exportedContext;
}
/**
 * Initializes objects in the context from a provided string
 * Contains fields needed for adding a new node through separate commands
 * @param ctx - accumulator object
 * @param ctxData - data in string format
 * @returns file writable object
 */
export function addLoadContextParser(context_, contextData) {
    const config = context_.config;
    context_.signingCertDer = new Uint8Array(contextData.signingCertDer.split(','));
    context_.gossipEndpoints = prepareEndpoints(context_.config.endpointType, contextData.gossipEndpoints, constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT);
    context_.grpcServiceEndpoints = prepareEndpoints(context_.config.endpointType, contextData.grpcServiceEndpoints, constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT);
    context_.adminKey = PrivateKey.fromStringED25519(contextData.adminKey);
    config.nodeAlias = contextData.newNode.name;
    config.existingNodeAliases = contextData.existingNodeAliases;
    config.allNodeAliases = [...config.existingNodeAliases, contextData.newNode.name];
    config.newNodeAliases = [contextData.newNode.name];
    const fieldsToImport = ['tlsCertHash', 'upgradeZipHash', 'newNode'];
    for (const property of fieldsToImport) {
        context_[property] = contextData[property];
    }
}
export function prepareEndpoints(endpointType, endpoints, defaultPort) {
    const returnValue = [];
    for (const endpoint of endpoints) {
        const parts = endpoint.split(':');
        let url = '';
        let port = defaultPort;
        if (parts.length === 2) {
            url = parts[0].trim();
            port = +parts[1].trim();
        }
        else if (parts.length === 1) {
            url = parts[0];
        }
        else {
            throw new SoloError(`incorrect endpoint format. expected url:port, found ${endpoint}`);
        }
        if (endpointType.toUpperCase() === constants.ENDPOINT_TYPE_IP) {
            returnValue.push(new ServiceEndpoint({
                port: +port,
                ipAddressV4: parseIpAddressToUint8Array(url),
            }));
        }
        else {
            returnValue.push(new ServiceEndpoint({
                port: +port,
                domainName: url,
            }));
        }
    }
    return returnValue;
}
/** Adds all the types of flags as properties on the provided argv object */
export function addFlagsToArgv(argv, flags) {
    argv.required = flags.required;
    argv.optional = flags.optional;
    return argv;
}
export function resolveValidJsonFilePath(filePath, defaultPath) {
    if (!filePath) {
        if (defaultPath) {
            return resolveValidJsonFilePath(defaultPath, null);
        }
        return '';
    }
    const resolvedFilePath = PathEx.realPathSync(filePath);
    if (!fs.existsSync(resolvedFilePath)) {
        if (defaultPath) {
            return resolveValidJsonFilePath(defaultPath, null);
        }
        throw new SoloError(`File does not exist: ${filePath}`);
    }
    // If the file is empty (or size cannot be determined) then fallback on the default values
    const throttleInfo = fs.statSync(resolvedFilePath);
    if (throttleInfo.size === 0 && defaultPath) {
        return resolveValidJsonFilePath(defaultPath, null);
    }
    else if (throttleInfo.size === 0) {
        throw new SoloError(`File is empty: ${filePath}`);
    }
    try {
        // Ensure the file contains valid JSON data
        JSON.parse(fs.readFileSync(resolvedFilePath, 'utf8'));
        return resolvedFilePath;
    }
    catch {
        // Fallback to the default values if an error occurs due to invalid JSON data or unable to read the file size
        if (defaultPath) {
            return resolveValidJsonFilePath(defaultPath, null);
        }
        throw new SoloError(`Invalid JSON data in file: ${filePath}`);
    }
}
export function prepareValuesFiles(valuesFile) {
    let valuesArgument = '';
    if (valuesFile) {
        const valuesFiles = valuesFile.split(',');
        for (const vf of valuesFiles) {
            const vfp = PathEx.resolve(vf);
            valuesArgument += ` --values ${vfp}`;
        }
    }
    return valuesArgument;
}
export function populateHelmArguments(valuesMapping) {
    let valuesArgument = '';
    for (const [key, value] of Object.entries(valuesMapping)) {
        valuesArgument += ` --set ${key}=${value}`;
    }
    return valuesArgument;
}
/**
 * @param nodeAlias
 * @param consensusNodes
 * @returns context of the node
 */
export function extractContextFromConsensusNodes(nodeAlias, consensusNodes) {
    if (!consensusNodes) {
        return undefined;
    }
    if (consensusNodes.length === 0) {
        return undefined;
    }
    const consensusNode = consensusNodes.find(node => node.name === nodeAlias);
    return consensusNode ? consensusNode.context : undefined;
}
/**
 * Check if the namespace exists in the context of given consensus nodes
 * @param consensusNodes
 * @param k8Factory
 * @param namespace
 */
export async function checkNamespace(consensusNodes, k8Factory, namespace) {
    for (const consensusNode of consensusNodes) {
        const k8 = k8Factory.getK8(consensusNode.context);
        if (!(await k8.namespaces().has(namespace))) {
            throw new SoloError(`namespace ${namespace} does not exist in context ${consensusNode.context}`);
        }
    }
}
/**
 * Print a message and pad both sides with asterisks to make it stand out
 * @param message The message to print
 * @param totalWidth The total width of the padded message
 */
function printPaddedMessage(message, totalWidth) {
    // If the message is longer than or equal to totalWidth, return it as is
    if (message.length >= totalWidth) {
        return message;
    }
    // Calculate the total padding needed (excluding the message length)
    const totalPadding = totalWidth - message.length;
    // Split the padding between left and right (favoring left if odd)
    const leftPadding = Math.floor(totalPadding / 2);
    const rightPadding = totalPadding - leftPadding;
    // Construct the padded string
    return '*'.repeat(leftPadding) + message + '*'.repeat(rightPadding);
}
/**
 * Show a banner with the chart name and version
 * @param logger
 * @param chartName The name of the chart
 * @param version The version of the chart
 * @param type The action that was performed such as 'Installed' or 'Upgraded'
 */
// TODO convert usages to leverage the logger.addMessageGroupMessage()
export function showVersionBanner(logger, chartName, version, type = 'Installed') {
    logger.showUser(chalk.cyan(` - ${type} ${chartName} chart, version:`, chalk.yellow(version)));
}
/**
 * Check if the input is a valid IPv4 address
 * @param input
 * @returns true if the input is a valid IPv4 address, false otherwise
 */
export function isIpV4Address(input) {
    const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)$/;
    return ipv4Regex.test(input);
}
/**
 * Convert an IPv4 address to a base64 string
 * @param ipv4 The IPv4 address to convert
 * @returns The base64 encoded string representation of the IPv4 address
 */
export function ipV4ToBase64(ipv4) {
    // Split the IPv4 address into its octets
    const octets = ipv4.split('.').map(octet => {
        const number_ = Number.parseInt(octet, 10);
        // eslint-disable-next-line unicorn/prefer-number-properties
        if (isNaN(number_) || number_ < 0 || number_ > 255) {
            throw new Error(`Invalid IPv4 address: ${ipv4}`);
        }
        return number_;
    });
    if (octets.length !== 4) {
        throw new Error(`Invalid IPv4 address: ${ipv4}`);
    }
    // Convert the octets to a Uint8Array
    const uint8Array = new Uint8Array(octets);
    // Base64 encode the byte array
    return btoa(String.fromCodePoint(...uint8Array));
}
export function entityId(shard, realm, number) {
    return `${shard}.${realm}.${number}`;
}
export async function withTimeout(promise, duration, errorMessage = 'Timeout') {
    return Promise.race([promise, throwAfter(duration, errorMessage)]);
}
async function throwAfter(duration, message = 'Timeout') {
    await sleep(duration);
    throw new SoloError(message);
}
/**
 * Checks if a Docker image with the given name and tag exists locally.
 * @param imageName The name of the Docker image (e.g., "block-node-server").
 * @param imageTag The tag of the Docker image (e.g., "0.12.0").
 * @returns True if the image exists, false otherwise.
 */
export function checkDockerImageExists(imageName, imageTag) {
    const fullImageName = `${imageName}:${imageTag}`;
    try {
        // Execute the 'docker images' command and filter by the image name
        // The --format "{{.Repository}}:{{.Tag}}" ensures consistent output
        // We use grep to filter for the exact image:tag
        const command = `docker images --format "{{.Repository}}:{{.Tag}}" | grep -E "^${fullImageName}$"`;
        const output = execSync(command, { encoding: 'utf8', stdio: 'pipe' });
        return output.trim() === fullImageName;
    }
    catch (error) {
        console.error(`Error checking Docker image ${fullImageName}:`, error.message);
        return false;
    }
}
export function createDirectoryIfNotExists(file) {
    const directory = file.slice(0, Math.max(0, file.lastIndexOf('/')));
    if (!fs.existsSync(directory)) {
        fs.mkdirSync(directory, { recursive: true });
    }
}
export async function findMinioOperator(context, k8) {
    const minioTenantPod = await k8
        .getK8(context)
        .pods()
        .listForAllNamespaces(['app.kubernetes.io/name=operator', 'operator=leader'])
        .then((pods) => pods[0]);
    if (!minioTenantPod) {
        return {
            exists: false,
            releaseName: undefined,
        };
    }
    return {
        exists: true,
        releaseName: minioTenantPod.labels?.['app.kubernetes.io/instance'],
    };
}
export function remoteConfigsToDeploymentsTable(remoteConfigs) {
    const rows = [];
    if (remoteConfigs.length > 0) {
        rows.push('Namespace : deployment');
        for (const remoteConfig of remoteConfigs) {
            const remoteConfigData = yaml.parse(remoteConfig.data['remote-config-data']);
            for (const cluster of remoteConfigData.clusters) {
                rows.push(`${remoteConfig.namespace.name} : ${cluster.deployment}`);
            }
        }
    }
    return rows;
}
//# sourceMappingURL=helpers.js.map