// SPDX-License-Identifier: Apache-2.0
import { container } from 'tsyringe-neo';
import { IllegalArgumentError } from '../../../../../core/errors/illegal-argument-error.js';
import { MissingArgumentError } from '../../../../../core/errors/missing-argument-error.js';
import { SoloError } from '../../../../../core/errors/solo-error.js';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import { spawn } from 'node:child_process';
import { v4 as uuid4 } from 'uuid';
import * as tar from 'tar';
import { InjectTokens } from '../../../../../core/dependency-injection/inject-tokens.js';
import { sleep } from '../../../../../core/helpers.js';
import { Duration } from '../../../../../core/time/duration.js';
import * as constants from '../../../../../core/constants.js';
export class K8ClientContainer {
    kubeConfig;
    containerReference;
    pods;
    logger;
    constructor(kubeConfig, containerReference, pods) {
        this.kubeConfig = kubeConfig;
        this.containerReference = containerReference;
        this.pods = pods;
        this.logger = container.resolve(InjectTokens.SoloLogger);
    }
    async getContext() {
        return this.kubeConfig.getCurrentContext();
    }
    async execKubectl(arguments_, outputPassThroughStream, errorPassThroughStream) {
        const context = await this.getContext();
        const fullArguments = ['--context', context, ...arguments_];
        return new Promise((resolve, reject) => {
            const process = spawn('kubectl', fullArguments, {
                stdio: ['ignore', 'pipe', 'pipe'],
            });
            let stdout = '';
            let stderr = '';
            process.stdout.on('data', (chunk) => {
                if (outputPassThroughStream) {
                    outputPassThroughStream.write(chunk);
                }
                stdout += chunk.toString();
            });
            process.stderr.on('data', (chunk) => {
                if (errorPassThroughStream) {
                    errorPassThroughStream.write(chunk);
                }
                stderr += chunk.toString();
            });
            process.on('error', (error) => {
                reject(new SoloError(`container call failed to start: ${error?.message}`));
            });
            process.on('close', (code) => {
                if (code === 0) {
                    resolve(stdout || stderr);
                }
                else {
                    reject(new SoloError(`container call failed: ${stderr || stdout}`));
                }
            });
        });
    }
    /**
     * Execute `kubectl cp` with retries and optional verification.
     *
     * @param source - kubectl cp source, e.g. `<ns>/<pod>:/path` or `/local/path`
     * @param destination - kubectl cp destination, e.g. `/local/path` or `<ns>/<pod>:/path`
     * @param containerName - name of the container for -c flag
     * @param verifyPath - local filesystem path to verify after copy (usually the destination for copyFrom)
     * @param expectedSize - optional expected file size for strict verification
     */
    async execKubectlCp(source, destination, containerName, verifyPath, expectedSize) {
        const maxAttempts = constants.CONTAINER_COPY_MAX_ATTEMPTS;
        const arguments_ = ['cp', source, destination, '-c', containerName];
        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                await this.execKubectl(arguments_);
                if (!fs.existsSync(verifyPath)) {
                    throw new SoloError(`copy failed: missing file at ${verifyPath}`);
                }
                const stat = fs.statSync(verifyPath);
                if (expectedSize !== undefined && stat.size !== expectedSize) {
                    throw new SoloError(`copy verification failed: expected size ${expectedSize} but found ${stat.size} at ${verifyPath}`);
                }
                return;
            }
            catch (error) {
                if (attempt === maxAttempts) {
                    throw error;
                }
                // backoff between retries
                await sleep(Duration.ofMillis(attempt * constants.CONTAINER_COPY_BACKOFF_MS));
            }
        }
    }
    async copyFrom(sourcePath, destinationDirectory) {
        const namespace = this.containerReference.parentReference.namespace;
        const podName = this.containerReference.parentReference.name.toString();
        const containerName = this.containerReference.name.toString();
        if (!(await this.pods.read(this.containerReference.parentReference))) {
            throw new IllegalArgumentError(`Invalid pod ${podName}`);
        }
        if (!fs.existsSync(destinationDirectory)) {
            throw new SoloError(`invalid destination path: ${destinationDirectory}`);
        }
        this.logger.info(`copyFrom: [srcPath=${sourcePath}, destDir=${destinationDirectory}] from ${namespace.name}/${podName}:${containerName}`);
        let entries = await this.listDir(sourcePath);
        if (entries.length !== 1) {
            throw new SoloError(`copyFrom: invalid source path: ${sourcePath}`);
        }
        // handle symbolic link
        if (entries[0].name.includes(' -> ')) {
            const arrowIndex = entries[0].name.indexOf(' -> ');
            const targetSuffix = entries[0].name.slice(arrowIndex + 4);
            const redirectSourcePath = `${path.dirname(sourcePath)}/${targetSuffix}`;
            entries = await this.listDir(redirectSourcePath);
            if (entries.length !== 1) {
                throw new SoloError(`copyFrom: invalid source path: ${redirectSourcePath}`);
            }
        }
        const sourceFileDesc = entries[0];
        const sourceFileSize = Number.parseInt(sourceFileDesc.size, 10);
        const resolvedRemotePath = sourceFileDesc.name;
        const sourceFileName = path.basename(resolvedRemotePath);
        const destinationPath = path.join(destinationDirectory, sourceFileName);
        this.logger.info(`copyFrom: beginning copy [container: ${containerName} ${namespace.name}/${podName}:${resolvedRemotePath} ${destinationPath}]`);
        const remoteSource = `${namespace.name}/${podName}:${resolvedRemotePath}`;
        await this.execKubectlCp(remoteSource, destinationPath, containerName, destinationPath, sourceFileSize);
        return true;
    }
    async copyTo(sourcePath, destinationDirectory, filter) {
        const namespace = this.containerReference.parentReference.namespace;
        const podName = this.containerReference.parentReference.name.toString();
        const containerName = this.containerReference.name.toString();
        if (!(await this.pods.read(this.containerReference.parentReference))) {
            throw new IllegalArgumentError(`Invalid pod ${podName}`);
        }
        if (!(await this.hasDir(destinationDirectory))) {
            throw new SoloError(`invalid destination path: ${destinationDirectory}`);
        }
        if (!fs.existsSync(sourcePath)) {
            throw new SoloError(`invalid source path: ${sourcePath}`);
        }
        const remoteDestination = `${namespace.name}/${podName}:${destinationDirectory}`;
        this.logger.info(`copyTo: [srcPath=${sourcePath}, destDir=${destinationDirectory}] to ${remoteDestination} (container=${containerName})`);
        let localPathToCopy = sourcePath;
        let temporaryDirectory;
        let temporaryTar;
        try {
            if (filter) {
                const sourceFileName = path.basename(sourcePath);
                const sourceDirectory = path.dirname(sourcePath);
                temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'solo-kubectl-cp-src-'));
                temporaryTar = path.join(temporaryDirectory, `${sourceFileName}-${uuid4()}.tar`);
                // Create a filtered tarball
                await tar.c({ file: temporaryTar, cwd: sourceDirectory, filter }, [sourceFileName]);
                // Extract the filtered content into the temporaryDirectory.
                await tar.x({ file: temporaryTar, cwd: temporaryDirectory });
                localPathToCopy = path.join(temporaryDirectory, sourceFileName);
                if (!fs.existsSync(localPathToCopy)) {
                    throw new SoloError(`filtered source path does not exist: ${localPathToCopy}`);
                }
            }
            this.logger.info(`copyTo: beginning copy [container: ${containerName} ${localPathToCopy} ${remoteDestination}]`);
            await this.execKubectlCp(localPathToCopy, remoteDestination, containerName, localPathToCopy);
            return true;
        }
        finally {
            // Clean up temp artifacts if any.
            if (temporaryTar && fs.existsSync(temporaryTar)) {
                try {
                    fs.rmSync(temporaryTar);
                }
                catch {
                    // ignore
                }
            }
            if (temporaryDirectory && fs.existsSync(temporaryDirectory)) {
                try {
                    fs.rmSync(temporaryDirectory, { recursive: true, force: true });
                }
                catch {
                    // ignore
                }
            }
        }
    }
    async execContainer(cmd, outputPassThroughStream, errorPassThroughStream) {
        const namespace = this.containerReference.parentReference.namespace;
        const podName = this.containerReference.parentReference.name.toString();
        const containerName = this.containerReference.name.toString();
        if (!(await this.pods.read(this.containerReference.parentReference))) {
            throw new IllegalArgumentError(`Invalid pod ${podName}`);
        }
        if (!cmd) {
            throw new MissingArgumentError('command cannot be empty');
        }
        const command = Array.isArray(cmd) ? cmd : cmd.split(' ');
        this.logger.info(`execContainer: beginning call [podName: ${podName} -n ${namespace.name} -c ${containerName} -- ${command.join(' ')}]`);
        const arguments_ = ['exec', podName, '-n', namespace.name, '-c', containerName, '--', ...command];
        return await this.execKubectl(arguments_, outputPassThroughStream, errorPassThroughStream);
    }
    async hasDir(destinationPath) {
        const bashScript = `[[ -d "${destinationPath}" ]] && echo -n "true" || echo -n "false"`;
        try {
            const result = await this.execContainer(['bash', '-c', bashScript]);
            return result === 'true';
        }
        catch (error) {
            this.logger.debug(`hasDir failed using bash for ${this.containerReference.parentReference.name}:${this.containerReference.name}, retrying with /bin/sh`, error);
            const shScript = `[ -d "${destinationPath}" ] && echo -n "true" || echo -n "false"`;
            const result = await this.execContainer(['/bin/sh', '-c', shScript]);
            return result === 'true';
        }
    }
    async hasFile(destinationPath, filters = {}) {
        const parentDirectory = path.dirname(destinationPath);
        const fileName = path.basename(destinationPath);
        const filterMap = new Map(Object.entries(filters));
        try {
            const entries = await this.listDir(parentDirectory);
            for (const item of entries) {
                if (item.name === fileName && !item.directory) {
                    let found = true;
                    for (const [field, value] of filterMap.entries()) {
                        this.logger.debug(`Checking file ${this.containerReference.parentReference.name}:${this.containerReference.name} ${destinationPath}; ${field} expected ${value}, found ${item[field]}`, { filters });
                        if (`${value}` !== `${item[field]}`) {
                            found = false;
                            break;
                        }
                    }
                    if (found) {
                        this.logger.debug(`File check succeeded ${this.containerReference.parentReference.name}:${this.containerReference.name} ${destinationPath}`, {
                            filters,
                        });
                        return true;
                    }
                }
            }
        }
        catch (error) {
            throw new SoloError(`unable to check file in '${this.containerReference.parentReference.name}':${this.containerReference.name}' - ${destinationPath}: ${error.message}`, error);
        }
        return false;
    }
    async listDir(destinationPath) {
        try {
            const output = await this.execContainer(['ls', '-la', destinationPath]);
            if (!output) {
                return [];
            }
            // parse the output and return the entries
            const items = [];
            const lines = output.split('\n');
            for (let line of lines) {
                line = line.replaceAll(/\s+/g, '|');
                const parts = line.split('|');
                if (parts.length >= 9) {
                    let name = parts.at(-1);
                    // handle unique file format (without single quotes): 'usedAddressBook_vHederaSoftwareVersion{hapiVersion=v0.53.0, servicesVersion=v0.53.0}_2024-07-30-20-39-06_node_0.txt.debug'
                    for (let index = parts.length - 1; index > 8; index--) {
                        name = `${parts[index - 1]} ${name}`;
                    }
                    if (name !== '.' && name !== '..') {
                        const permission = parts[0];
                        const item = {
                            directory: permission[0] === 'd',
                            owner: parts[2],
                            group: parts[3],
                            size: parts[4],
                            modifiedAt: `${parts[5]} ${parts[6]} ${parts[7]}`,
                            name,
                        };
                        items.push(item);
                    }
                }
            }
            return items;
        }
        catch (error) {
            throw new SoloError(`unable to check path in '${this.containerReference.parentReference.name}':${this.containerReference.name}' - ${destinationPath}: ${error.message}`, error);
        }
    }
    async mkdir(destinationPath) {
        return this.execContainer(['bash', '-c', `mkdir -p "${destinationPath}"`]);
    }
}
//# sourceMappingURL=k8-client-container.js.map