import { glob } from 'glob';
import logUpdate from 'log-update';
import tar from 'tar-fs';
import constants from '../constants.js';
import OutputManager from '../outputManager.js';
import Container from './container.js';
export default class Image {
    _IMAGE_PLATFORM_MSG_REGEX = /The requested image's platform \(.*\) does not match the detected host platform \(.*\) and no specific platform was requested/;
    docker;
    name;
    constructor(docker, name) {
        this.name = name;
        this.docker = docker;
    }
    async build(srcPaths, srcRoot, dockerfileContent) {
        const matchedFiles = new Set();
        if (srcPaths) {
            for (const pattern of srcPaths) {
                const files = glob.sync(pattern, { cwd: srcRoot });
                for (const file of files) {
                    OutputManager.detail(`Adding file to build context: ${file}`);
                    matchedFiles.add(file);
                }
            }
        }
        const files = [...matchedFiles];
        const archive = tar.pack(srcRoot, {
            entries: files, finalize: false, finish(a) {
                a.entry({ name: "Dockerfile" }, dockerfileContent);
                a.finalize();
            }
        });
        await new Promise((resolve, reject) => {
            this.docker.buildImage(archive, { platform: constants.RUNTIME_ARCH, t: this.name }, (err, stream) => this.outputStreamHandler({ errors: false }, err, stream, resolve, reject));
        });
    }
    async getFiles(localDestBaseDir, remotePaths) {
        await this.withContainer({}, async (container) => {
            await container.getFiles(localDestBaseDir, remotePaths);
        });
    }
    getName() {
        return this.name;
    }
    async pull(imageUrl = this.name, username, password) {
        await new Promise((resolve, reject) => {
            const state = { errors: false };
            this.docker.pull(imageUrl, { authconfig: { password, username }, platform: constants.RUNTIME_ARCH }, (err, stream) => this.outputStreamHandler(state, err, stream, resolve, reject));
        });
    }
    async withContainer(containerParams = {}, fn) {
        const container = new Container({
            docker: this.docker,
            imageName: this.name,
            ...containerParams
        });
        await container.create();
        try {
            await fn(container);
        }
        finally {
            await container.destroy();
        }
    }
    // eslint-disable-next-line max-params, @typescript-eslint/no-explicit-any
    async outputStreamHandler(state, err, stream, resolve, reject) {
        if (err) {
            reject(err);
            return;
        }
        if (!stream) {
            reject(new Error("No data received from Docker"));
            return;
        }
        stream.setEncoding('utf8');
        // Our buffer accumulates chunks.
        let buffer = "";
        // A statuses lookup for progress/status messages.
        const statuses = {};
        // A helper function to process a fully parsed JSON message.
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const processMessage = (parsedMsg) => {
            const errorText = parsedMsg.error?.trim();
            const streamText = parsedMsg.stream?.trim();
            if (errorText) {
                state.errors = true;
                OutputManager.newline();
                OutputManager.detail(`${constants.ANSI_CODES.RED}${errorText}${constants.ANSI_CODES.RESET}`);
            }
            else if (streamText && !this._IMAGE_PLATFORM_MSG_REGEX.test(streamText)) {
                OutputManager.detail(streamText);
            }
            else if (parsedMsg.id) {
                const thisStatus = { id: parsedMsg.id, status: parsedMsg.status };
                if (parsedMsg.progressDetail) {
                    const progressCurrent = Number(parsedMsg.progressDetail.current);
                    const progressTotal = Number(parsedMsg.progressDetail.total);
                    if (progressTotal > 0) {
                        thisStatus.percentage = (progressCurrent / progressTotal) * 100;
                    }
                }
                statuses[parsedMsg.id] = thisStatus;
                const statusText = Object.values(statuses)
                    .map((status) => status.percentage
                    ? `${status.id}: ${status.status} (${status.percentage.toFixed(0)}%)`
                    : `${status.id}: ${status.status}`)
                    .join("\n");
                logUpdate(constants.ANSI_CODES.DIM + statusText + constants.ANSI_CODES.RESET);
            }
        };
        stream.on('data', (chunk) => {
            // The buffer is expected to contain one or more JSON objects, each with a trailing newline.
            // After splitting, the last line will be blank if all the lines are complete JSON objects.
            // If the last line is in fact not blank, that probably means it's an incomplete JSON fragment.
            // This means we need to keep it in the buffer in the hope that a subsequent chunk will complete it.
            buffer += chunk;
            const lines = buffer.split("\n");
            buffer = lines.pop() || "";
            for (const line of lines) {
                if (line.trim() === "")
                    continue;
                try {
                    const parsed = JSON.parse(line);
                    processMessage(parsed);
                }
                catch {
                    buffer = line + buffer;
                    OutputManager.detail(`${constants.ANSI_CODES.YELLOW}Warning: Incomplete or malformed data received, waiting for more data${constants.ANSI_CODES.RESET}`);
                    break;
                }
            }
        });
        stream.on('end', () => {
            if (buffer.trim()) {
                try {
                    const parsed = JSON.parse(buffer);
                    processMessage(parsed);
                }
                catch {
                    OutputManager.detail(`${constants.ANSI_CODES.YELLOW}Warning: Unable to parse leftover JSON at stream end: ${buffer}${constants.ANSI_CODES.RESET}`);
                }
            }
            logUpdate.done();
            OutputManager.newline();
            if (state.errors) {
                reject(new Error("Docker operation failed"));
            }
            else {
                resolve();
            }
        });
        stream.on('close', () => {
            logUpdate.done();
        });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        stream.on('error', (streamErr) => {
            reject(streamErr);
        });
    }
}
