import { createHash, createPublicKey, randomUUID } from 'node:crypto';
import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { extname, isAbsolute, join, resolve } from 'node:path';
import { NPM_PACKAGE_NAME_PATTERN, SnapshotBuildFileSchema, snapshotBuildFinalize, snapshotBuildInit, snapshotUpload, } from '@agentuity/server';
import { YAML } from 'bun';
import * as tar from 'tar';
import { z } from 'zod';
import { getCommand } from '../../../../command-prefix';
import { getCatalystAPIClient } from '../../../../config';
import { encryptFIPSKEMDEMStream } from '../../../../crypto/box';
import { ErrorCode, getExitCode } from '../../../../errors';
import * as tui from '../../../../tui';
import { createCommand } from '../../../../types';
import { validateAptDependencies } from '../../../../utils/apt-validator';
import { getGitInfo, mergeGitInfo } from '../../../../utils/git';
export const SNAPSHOT_TAG_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
export const MAX_SNAPSHOT_TAG_LENGTH = 128;
const SnapshotBuildResponseSchema = z.object({
    snapshotId: z.string().describe('Snapshot ID'),
    name: z.string().describe('Snapshot name'),
    tag: z.string().nullable().optional().describe('Snapshot tag'),
    runtime: z.string().describe('Runtime identifier'),
    sizeBytes: z.number().describe('Snapshot size in bytes'),
    fileCount: z.number().describe('Number of files in snapshot'),
    createdAt: z.string().describe('Snapshot creation timestamp'),
    unchanged: z.boolean().optional().describe('True if snapshot was unchanged'),
    userMetadata: z
        .record(z.string(), z.string())
        .optional()
        .describe('User-defined metadata key-value pairs'),
    error: z.string().optional().describe('Error message if build failed'),
    malwareDetected: z.boolean().optional().describe('True if malware was detected'),
    virusName: z.string().optional().describe('Name of detected virus'),
});
const MALWARE_REGEX = /malware detected \(([^)]+)\)/i;
function buildFileTree(files) {
    const root = { name: '', isFile: false, children: new Map() };
    for (const file of files) {
        const parts = file.path.split('/');
        let current = root;
        for (let i = 0; i < parts.length; i++) {
            const part = parts[i];
            if (!part)
                continue;
            if (!current.children.has(part)) {
                current.children.set(part, {
                    name: part,
                    isFile: i === parts.length - 1,
                    children: new Map(),
                });
            }
            const child = current.children.get(part);
            if (!child)
                continue;
            current = child;
            if (i === parts.length - 1) {
                current.size = file.size;
                current.isFile = true;
            }
        }
    }
    return root;
}
function printFileTree(files) {
    const tree = buildFileTree(files);
    printTreeNode(tree, '  ');
}
function printTreeNode(node, prefix) {
    const entries = Array.from(node.children.entries()).sort((a, b) => {
        const aIsDir = !a[1].isFile;
        const bIsDir = !b[1].isFile;
        if (aIsDir !== bIsDir)
            return aIsDir ? -1 : 1;
        return a[0].localeCompare(b[0]);
    });
    for (let i = 0; i < entries.length; i++) {
        const entry = entries[i];
        if (!entry)
            continue;
        const [, child] = entry;
        const isLast = i === entries.length - 1;
        const connector = tui.muted(isLast ? '└── ' : '├── ');
        const sizeStr = child.isFile && child.size !== undefined ? ` (${tui.formatBytes(child.size)})` : '';
        console.log(`${prefix}${connector}${child.name}${sizeStr}`);
        if (child.children.size > 0) {
            const newPrefix = prefix + (isLast ? '    ' : tui.muted('│   '));
            printTreeNode(child, newPrefix);
        }
    }
}
function parseKeyValueArgs(args) {
    if (!args || args.length === 0) {
        return {};
    }
    const result = {};
    for (const arg of args) {
        const eqIndex = arg.indexOf('=');
        if (eqIndex === -1) {
            throw new Error(`Invalid KEY=VALUE format: "${arg}"`);
        }
        const key = arg.slice(0, eqIndex);
        const value = arg.slice(eqIndex + 1);
        if (!key) {
            throw new Error(`Invalid KEY=VALUE format: "${arg}" (empty key)`);
        }
        result[key] = value;
    }
    return result;
}
function substituteVariables(values, variables, flagName) {
    const result = {};
    const varPattern = /\$\{([^}]+)\}/g;
    for (const [key, value] of Object.entries(values)) {
        let substituted = value;
        varPattern.lastIndex = 0;
        let match = varPattern.exec(value);
        while (match !== null) {
            const varName = match[1];
            if (!varName || !(varName in variables)) {
                throw new Error(`Variable "\${${varName}}" in "${key}" is not defined. Use --${flagName} ${varName}=value to provide it.`);
            }
            const varValue = variables[varName];
            if (varValue !== undefined) {
                substituted = substituted.replace(match[0], varValue);
            }
            match = varPattern.exec(value);
        }
        result[key] = substituted;
    }
    return result;
}
// Default patterns that are always excluded from snapshot builds
export const DEFAULT_EXCLUSIONS = ['.git', '.git/**', 'node_modules/**', '.agentuity/**', '.env*'];
export async function resolveFileGlobs(directory, patterns) {
    const files = new Map();
    const exclusions = [...DEFAULT_EXCLUSIONS];
    const inclusions = [];
    for (const pattern of patterns) {
        if (pattern.startsWith('!')) {
            exclusions.push(pattern.slice(1));
        }
        else {
            inclusions.push(pattern);
        }
    }
    for (const pattern of inclusions) {
        const glob = new Bun.Glob(pattern);
        for await (const file of glob.scan({ cwd: directory, dot: true })) {
            const absolutePath = join(directory, file);
            try {
                const stat = statSync(absolutePath);
                if (stat.isFile()) {
                    files.set(file, {
                        path: file,
                        absolutePath,
                        size: stat.size,
                    });
                }
            }
            catch (err) {
                // Skip files that can't be stat'd (broken symlinks, permission issues, etc.)
                console.debug(`Skipping ${file}: ${err instanceof Error ? err.message : err}`);
            }
        }
    }
    // Expand exclusion patterns to include nested variants
    const expandedExclusions = [];
    for (let pattern of exclusions) {
        // If pattern already contains glob wildcards, use it as-is
        // Otherwise, check if it refers to a directory and auto-append /** to exclude all contents
        const hasGlobChars = /[*?[\]]/.test(pattern);
        if (!hasGlobChars) {
            const patternPath = join(directory, pattern);
            try {
                const stat = statSync(patternPath);
                if (stat.isDirectory()) {
                    pattern = pattern.endsWith('/') ? `${pattern}**` : `${pattern}/**`;
                }
            }
            catch {
                // Path doesn't exist or can't be stat'd, use pattern as-is
            }
        }
        expandedExclusions.push(pattern);
        // Add **/ prefix variant to match nested occurrences (e.g., .agentuity/** -> **/.agentuity/**)
        // Skip if pattern already starts with **/ or is just a wildcard pattern
        if (!pattern.startsWith('**/')) {
            expandedExclusions.push(`**/${pattern}`);
        }
    }
    for (const pattern of expandedExclusions) {
        const glob = new Bun.Glob(pattern);
        for await (const file of glob.scan({ cwd: directory, dot: true })) {
            files.delete(file);
        }
    }
    return files;
}
async function createTarGzArchive(directory, files, outputPath) {
    const filePaths = Array.from(files.keys());
    await tar.create({
        gzip: true,
        file: outputPath,
        cwd: directory,
    }, filePaths);
}
function createProgressStream(file, totalSize, onProgress) {
    let bytesRead = 0;
    const reader = file.stream().getReader();
    return new ReadableStream({
        async pull(controller) {
            const { done, value } = await reader.read();
            if (done) {
                controller.close();
                onProgress(100);
                return;
            }
            bytesRead += value.byteLength;
            const percent = Math.min(99, Math.floor((bytesRead / totalSize) * 100));
            onProgress(percent);
            controller.enqueue(value);
        },
        cancel() {
            reader.cancel();
        },
    });
}
async function generateContentHash(params) {
    const hash = createHash('sha256');
    hash.update(`runtime:${params.runtime}\n`);
    if (params.description) {
        hash.update(`description:${params.description}\n`);
    }
    if (params.dependencies && params.dependencies.length > 0) {
        const sortedDeps = [...params.dependencies].sort();
        hash.update(`dependencies:${sortedDeps.join(',')}\n`);
    }
    if (params.packages && params.packages.length > 0) {
        const sortedPkgs = [...params.packages].sort();
        hash.update(`packages:${sortedPkgs.join(',')}\n`);
    }
    if (params.files.length > 0) {
        const sortedFiles = [...params.files].sort((a, b) => a.path.localeCompare(b.path));
        for (const file of sortedFiles) {
            const contentHash = params.fileHashes.get(file.path) ?? '';
            const mode = file.mode.toString(8).padStart(4, '0');
            hash.update(`file:${file.path}:${file.size}:${contentHash}:${mode}:${file.contentType}\n`);
        }
    }
    if (params.env && Object.keys(params.env).length > 0) {
        const sortedKeys = Object.keys(params.env).sort();
        for (const key of sortedKeys) {
            hash.update(`env:${key}=${params.env[key]}\n`);
        }
    }
    hash.update(`access:${params.isPublic ? 'public' : 'private'}\n`);
    return hash.digest('hex');
}
export const buildSubcommand = createCommand({
    name: 'build',
    description: 'Build a snapshot from a declarative file',
    tags: ['slow', 'requires-auth'],
    requires: { auth: true, org: true, region: true },
    examples: [
        {
            command: getCommand('cloud sandbox snapshot build .'),
            description: 'Build a snapshot from the current directory using agentuity-snapshot.yaml',
        },
        {
            command: getCommand('cloud sandbox snapshot build ./project --file custom-build.yaml'),
            description: 'Build using a custom build file',
        },
        {
            command: getCommand('cloud sandbox snapshot build . --env API_KEY=secret --tag production'),
            description: 'Build with environment variable substitution and custom tag',
        },
        {
            command: getCommand('cloud sandbox snapshot build . --dry-run'),
            description: 'Validate the build file without uploading',
        },
        {
            command: getCommand('cloud sandbox snapshot build . --force'),
            description: 'Force rebuild even if content is unchanged',
        },
    ],
    schema: {
        args: z.object({
            directory: z.string().describe('Directory containing files to include in snapshot'),
        }),
        options: z.object({
            file: z
                .string()
                .optional()
                .describe('Path to build file (defaults to agentuity-snapshot.[json|yaml|yml])'),
            env: z
                .array(z.string())
                .optional()
                .describe('Environment variable substitution (KEY=VALUE)'),
            name: z.string().optional().describe('Snapshot name (overrides build file)'),
            tag: z.string().optional().describe('Snapshot tag (defaults to "latest")'),
            description: z.string().optional().describe('Snapshot description (overrides build file)'),
            message: z.string().optional().describe('Build message for this snapshot'),
            commit: z.string().optional().describe('Git commit SHA (auto-detected if not provided)'),
            branch: z.string().optional().describe('Git branch (auto-detected if not provided)'),
            repo: z.string().optional().describe('Git repo URL (auto-detected if not provided)'),
            provider: z
                .string()
                .optional()
                .describe('Git provider (github, gitlab, bitbucket - auto-detected)'),
            commitUrl: z.string().optional().describe('URL to the commit'),
            metadata: z.array(z.string()).optional().describe('Metadata key-value pairs (KEY=VALUE)'),
            force: z.boolean().optional().describe('Force rebuild even if content is unchanged'),
            public: z
                .boolean()
                .optional()
                .describe('Make snapshot public (enables virus scanning, no encryption)'),
            confirm: z
                .boolean()
                .optional()
                .describe('Confirm public snapshot publishing (required for --public)'),
        }),
        response: SnapshotBuildResponseSchema,
    },
    async handler(ctx) {
        const { args, opts, options, auth, region, config, logger, orgId } = ctx;
        const dryRun = options.dryRun === true;
        let directory = resolve(args.directory);
        if (!existsSync(directory)) {
            logger.fatal(`Directory not found: ${directory}`);
        }
        let buildFilePath;
        if (opts.file) {
            buildFilePath = resolve(opts.file);
            if (!existsSync(buildFilePath)) {
                logger.fatal(`Build file not found: ${buildFilePath}`);
            }
        }
        else {
            const candidates = [
                'agentuity-snapshot.yaml',
                'agentuity-snapshot.yml',
                'agentuity-snapshot.json',
            ];
            for (const candidate of candidates) {
                const candidatePath = join(directory, candidate);
                if (existsSync(candidatePath)) {
                    buildFilePath = candidatePath;
                    break;
                }
            }
            if (!buildFilePath) {
                logger.fatal(`No build file found. Expected one of: ${candidates.join(', ')} in ${directory}`);
            }
        }
        const buildFileContent = await Bun.file(buildFilePath).text();
        const ext = extname(buildFilePath).toLowerCase();
        let parsedBuildFile;
        try {
            if (ext === '.yaml' || ext === '.yml') {
                parsedBuildFile = YAML.parse(buildFileContent);
            }
            else if (ext === '.json') {
                parsedBuildFile = JSON.parse(buildFileContent);
            }
            else {
                logger.fatal(`Unsupported build file extension: ${ext}. Use .yaml, .yml, or .json`);
            }
        }
        catch (err) {
            logger.fatal(`Failed to parse build file: ${err instanceof Error ? err.message : err}`);
        }
        const validationResult = SnapshotBuildFileSchema.safeParse(parsedBuildFile);
        if (!validationResult.success) {
            tui.error(`Invalid build file at ${buildFilePath}:`);
            for (const issue of validationResult.error.issues) {
                const path = issue.path.length > 0 ? issue.path.join('.') : 'root';
                tui.bullet(`${path}: ${issue.message}`);
            }
            process.exit(1);
        }
        const buildConfig = validationResult.data;
        // If dir is specified in the build file, use it as the effective build context
        if (buildConfig.dir) {
            if (isAbsolute(buildConfig.dir)) {
                logger.fatal(`'dir' must be a relative path, got: ${buildConfig.dir}`);
            }
            const dirPath = resolve(directory, buildConfig.dir);
            if (!dirPath.startsWith(`${directory}/`) && dirPath !== directory) {
                logger.fatal(`'dir' resolves outside the build root: ${dirPath}`);
            }
            let isDir = false;
            try {
                isDir = statSync(dirPath).isDirectory();
            }
            catch {
                // path does not exist
            }
            if (!isDir) {
                logger.fatal(`Build context directory not found or is not a directory: ${dirPath} (specified by 'dir: ${buildConfig.dir}' in build file)`);
            }
            directory = dirPath;
        }
        // Determine if snapshot is public: CLI flag takes precedence, otherwise use build file
        const isPublic = opts.public === true || (opts.public === undefined && buildConfig.public === true);
        if (isPublic && !dryRun) {
            if (!opts.confirm) {
                if (!tui.isTTYLike()) {
                    logger.fatal('Publishing a public snapshot requires confirmation.\n\n' +
                        'Public snapshots make all environment variables and files publicly accessible.\n\n' +
                        'To proceed, add the --confirm flag:\n' +
                        `  ${getCommand('cloud sandbox snapshot build . --public --confirm')}\n\n` +
                        'To preview what will be published, use --dry-run first:\n' +
                        `  ${getCommand('cloud sandbox snapshot build . --public --dry-run')}`);
                }
                tui.warningBox('Public Snapshot', 'You are publishing a public snapshot.\n\n' +
                    'This will make all environment variables and\n' +
                    'files in the snapshot publicly accessible.\n\n' +
                    'Run with --dry-run to preview the contents.');
                console.log('');
                const confirmed = await tui.confirm('Proceed with public snapshot?', false);
                if (!confirmed) {
                    logger.fatal('Aborted');
                }
            }
        }
        if (opts.tag) {
            if (opts.tag.length > MAX_SNAPSHOT_TAG_LENGTH) {
                logger.fatal(`Invalid snapshot tag: must be at most ${MAX_SNAPSHOT_TAG_LENGTH} characters`);
            }
            if (!SNAPSHOT_TAG_REGEX.test(opts.tag)) {
                logger.fatal('Invalid snapshot tag: must only contain letters, numbers, dashes, underscores, and dots, and cannot start with a period or dash');
            }
        }
        let envSubstitutions = {};
        let metadataSubstitutions = {};
        try {
            envSubstitutions = parseKeyValueArgs(opts.env);
            metadataSubstitutions = parseKeyValueArgs(opts.metadata);
        }
        catch (err) {
            logger.fatal(err instanceof Error ? err.message : String(err));
            return undefined;
        }
        let finalEnv;
        let finalMetadata;
        // Name and Description: CLI options override build file
        const finalName = opts.name ?? buildConfig.name;
        const finalDescription = opts.description ?? buildConfig.description;
        try {
            if (buildConfig.env) {
                finalEnv = substituteVariables(buildConfig.env, envSubstitutions, 'env');
            }
            if (buildConfig.metadata) {
                finalMetadata = substituteVariables(buildConfig.metadata, metadataSubstitutions, 'metadata');
            }
        }
        catch (err) {
            logger.fatal(err instanceof Error ? err.message : String(err));
            return undefined;
        }
        if (buildConfig.dependencies && buildConfig.dependencies.length > 0) {
            const aptValidation = await tui.spinner({
                message: 'Validating apt dependencies...',
                type: 'simple',
                callback: async () => {
                    return await validateAptDependencies(buildConfig.dependencies, region, config, logger);
                },
            });
            if (aptValidation.invalid.length > 0) {
                tui.error('Invalid apt dependencies:');
                for (const pkg of aptValidation.invalid) {
                    tui.bullet(`${pkg.package}: ${pkg.error}`);
                    if (pkg.availableVersions && pkg.availableVersions.length > 0) {
                        console.log(`    Available versions: ${pkg.availableVersions.join(', ')}`);
                    }
                    console.log(`    Search: ${pkg.searchUrl}`);
                }
                process.exit(1);
            }
        }
        if (buildConfig.packages && buildConfig.packages.length > 0) {
            // Validate package specifiers using shared pattern (also enforced at schema level)
            const invalidPackages = buildConfig.packages.filter((pkg) => !NPM_PACKAGE_NAME_PATTERN.test(pkg));
            if (invalidPackages.length > 0) {
                tui.error('Invalid package specifiers:');
                for (const pkg of invalidPackages) {
                    tui.bullet(`${pkg}: must not contain whitespace, semicolons, backticks, pipes, or dollar signs`);
                }
                process.exit(1);
            }
        }
        let files = new Map();
        if (buildConfig.files && buildConfig.files.length > 0) {
            files = await resolveFileGlobs(directory, buildConfig.files);
        }
        const fileMetadata = new Map();
        for (const file of files.values()) {
            const fullPath = join(directory, file.path);
            const bunFile = Bun.file(fullPath);
            const content = await bunFile.arrayBuffer();
            const hash = createHash('sha256').update(Buffer.from(content)).digest('hex');
            const contentType = bunFile.type || 'application/octet-stream';
            const stat = statSync(fullPath);
            const mode = stat.mode & 0o7777; // Extract permission bits only
            fileMetadata.set(file.path, { sha256: hash, contentType, mode });
        }
        const fileHashes = new Map();
        for (const [path, meta] of fileMetadata) {
            fileHashes.set(path, meta.sha256);
        }
        const fileList = Array.from(files.values()).map((f) => {
            const meta = fileMetadata.get(f.path);
            return {
                path: f.path,
                size: f.size,
                sha256: meta?.sha256 ?? '',
                contentType: meta?.contentType ?? 'application/octet-stream',
                mode: meta?.mode ?? 0o644,
            };
        });
        const totalSize = fileList.reduce((sum, f) => sum + f.size, 0);
        const contentHash = await generateContentHash({
            runtime: buildConfig.runtime,
            description: finalDescription,
            dependencies: buildConfig.dependencies,
            packages: buildConfig.packages,
            files: fileList,
            fileHashes,
            env: finalEnv,
            isPublic,
        });
        if (dryRun) {
            if (!options.json) {
                tui.info(`${tui.bold('Dry Run')} - No upload will be performed`);
                console.log('');
                tui.table([
                    {
                        Name: finalName,
                        Description: finalDescription ?? '-',
                        Runtime: buildConfig.runtime,
                        Tag: opts.tag ?? 'latest',
                        Access: isPublic ? 'public' : 'private',
                        Size: tui.formatBytes(totalSize),
                        Files: fileList.length.toFixed(),
                    },
                ], ['Name', 'Description', 'Runtime', 'Tag', 'Access', 'Size', 'Files'], { layout: 'vertical', padStart: '  ' });
                if (buildConfig.dependencies && buildConfig.dependencies.length > 0) {
                    console.log('');
                    tui.header('Dependencies');
                    for (const dep of buildConfig.dependencies) {
                        console.log(`  ${tui.muted('•')} ${dep}`);
                    }
                }
                if (buildConfig.packages && buildConfig.packages.length > 0) {
                    console.log('');
                    tui.header('Packages (npm/bun)');
                    for (const pkg of buildConfig.packages) {
                        console.log(`  ${tui.muted('•')} ${pkg}`);
                    }
                }
                if (finalEnv && Object.keys(finalEnv).length > 0) {
                    console.log('');
                    tui.header('Environment');
                    for (const [envKey, envValue] of Object.entries(finalEnv)) {
                        console.log(`  ${tui.muted('•')} ${envKey}=${tui.maskSecret(envValue)}`);
                    }
                }
                if (fileList.length > 0) {
                    console.log('');
                    tui.header('Files');
                    printFileTree(fileList);
                }
            }
            return {
                snapshotId: '',
                name: finalName ?? '',
                tag: opts.tag ?? 'latest',
                runtime: buildConfig.runtime,
                sizeBytes: totalSize,
                fileCount: fileList.length,
                createdAt: new Date().toISOString(),
                userMetadata: finalMetadata,
            };
        }
        const tempDir = join(tmpdir(), `snapshot-build-${randomUUID()}`);
        const archivePath = join(tempDir, 'snapshot.tar.gz');
        try {
            await Bun.write(join(tempDir, '.placeholder'), '');
            if (files.size > 0) {
                await tui.spinner({
                    message: 'Creating archive...',
                    type: 'simple',
                    callback: async () => {
                        await createTarGzArchive(directory, files, archivePath);
                    },
                });
            }
            else {
                await tar.create({
                    gzip: true,
                    file: archivePath,
                    cwd: tempDir,
                }, ['.placeholder']);
            }
            const archiveFile = Bun.file(archivePath);
            const archiveSize = archiveFile.size;
            const client = getCatalystAPIClient(logger, auth, region, undefined, config);
            // Auto-detect git info and merge with CLI overrides
            const autoDetectedGit = await getGitInfo(directory, logger);
            const mergedGitInfo = mergeGitInfo(autoDetectedGit, {
                message: opts.message,
                commit: opts.commit,
                branch: opts.branch,
                repo: opts.repo,
                provider: opts.provider,
                commitUrl: opts.commitUrl,
            });
            // Build git info for API (only include if we have any git data)
            const hasGitInfo = mergedGitInfo.branch ||
                mergedGitInfo.commit ||
                mergedGitInfo.repo ||
                mergedGitInfo.provider ||
                mergedGitInfo.commitUrl;
            const gitInfo = hasGitInfo
                ? {
                    branch: mergedGitInfo.branch,
                    commit: mergedGitInfo.commit,
                    repo: mergedGitInfo.repo,
                    provider: mergedGitInfo.provider,
                    commitUrl: mergedGitInfo.commitUrl,
                }
                : undefined;
            const initResult = await tui.spinner({
                message: 'Initializing snapshot build...',
                clearOnSuccess: true,
                callback: async () => {
                    return await snapshotBuildInit(client, {
                        runtime: buildConfig.runtime,
                        name: finalName,
                        tag: opts.tag ?? 'latest',
                        description: finalDescription,
                        message: mergedGitInfo.message,
                        git: gitInfo,
                        contentHash,
                        force: opts.force,
                        encrypt: !isPublic,
                        public: isPublic,
                        orgId,
                    });
                },
            });
            if (initResult.unchanged) {
                if (!options.json) {
                    tui.success(`Snapshot unchanged ${tui.bold(initResult.existingId)}`);
                    console.log('');
                    tui.table([
                        {
                            Name: finalName,
                            Tag: opts.tag ?? 'latest',
                        },
                    ], ['Name', 'Tag'], { layout: 'vertical', padStart: '  ' });
                }
                return {
                    snapshotId: initResult.existingId,
                    name: initResult.existingName,
                    tag: initResult.existingTag ?? undefined,
                    runtime: buildConfig.runtime,
                    sizeBytes: totalSize,
                    fileCount: fileList.length,
                    createdAt: new Date().toISOString(),
                    unchanged: true,
                    userMetadata: finalMetadata,
                };
            }
            // Encrypt the archive if public key is provided (private snapshots only)
            let uploadPath = archivePath;
            let uploadSize = archiveSize;
            if (initResult.publicKey) {
                const encryptedPath = join(tempDir, 'snapshot.tar.gz.enc');
                await tui.spinner({
                    message: 'Encrypting snapshot...',
                    type: 'simple',
                    clearOnSuccess: true,
                    callback: async () => {
                        const publicKey = createPublicKey({
                            key: initResult.publicKey,
                            format: 'pem',
                            type: 'spki',
                        });
                        const src = createReadStream(archivePath);
                        const dst = createWriteStream(encryptedPath);
                        await encryptFIPSKEMDEMStream(publicKey, src, dst);
                        await new Promise((resolve, reject) => {
                            dst.once('finish', resolve);
                            dst.once('error', reject);
                            dst.end();
                        });
                    },
                });
                uploadPath = encryptedPath;
                uploadSize = Bun.file(encryptedPath).size;
            }
            if (initResult.uploadUrl) {
                // Private snapshot: upload directly to S3
                // Use Bun.file() directly as body - Bun sets Content-Length automatically from file size
                await tui.spinner({
                    message: 'Uploading snapshot...',
                    clearOnSuccess: true,
                    callback: async () => {
                        const uploadFile = Bun.file(uploadPath);
                        const response = await fetch(initResult.uploadUrl, {
                            method: 'PUT',
                            headers: {
                                'Content-Type': 'application/gzip',
                            },
                            body: uploadFile,
                        });
                        if (!response.ok) {
                            throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
                        }
                    },
                });
            }
            else {
                // Public snapshot: upload via Catalyst (with virus scanning)
                try {
                    await tui.spinner({
                        message: 'Uploading and scanning snapshot...',
                        type: 'progress',
                        clearOnSuccess: true,
                        clearOnError: true,
                        callback: async (updateProgress) => {
                            const uploadFile = Bun.file(uploadPath);
                            const progressStream = createProgressStream(uploadFile, uploadSize, updateProgress);
                            await snapshotUpload(client, {
                                snapshotId: initResult.snapshotId,
                                body: progressStream,
                                contentLength: uploadSize,
                                orgId,
                            });
                        },
                    });
                }
                catch (err) {
                    const errorMessage = err instanceof Error ? err.message : String(err);
                    const malwareMatch = MALWARE_REGEX.exec(errorMessage);
                    if (malwareMatch) {
                        const virusName = malwareMatch[1];
                        if (options.json) {
                            console.log(JSON.stringify({
                                snapshotId: '',
                                name: finalName ?? '',
                                tag: opts.tag ?? 'latest',
                                runtime: buildConfig.runtime,
                                sizeBytes: totalSize,
                                fileCount: fileList.length,
                                createdAt: new Date().toISOString(),
                                error: errorMessage,
                                malwareDetected: true,
                                virusName,
                            }, null, 2));
                            process.exit(getExitCode(ErrorCode.MALWARE_DETECTED));
                        }
                        console.log('');
                        tui.errorBox('Malware Detected', `Your snapshot was rejected because it contains malware.\n\nVirus: ${virusName}\n\nPlease remove the infected files and try again.`);
                        tui.fatal('Snapshot build failed due to malware detection', ErrorCode.MALWARE_DETECTED);
                    }
                    throw err;
                }
            }
            const snapshot = await tui.spinner({
                message: 'Finalizing snapshot...',
                clearOnSuccess: true,
                callback: async () => {
                    return await snapshotBuildFinalize(client, {
                        snapshotId: initResult.snapshotId,
                        sizeBytes: totalSize,
                        fileCount: fileList.length,
                        files: fileList,
                        dependencies: buildConfig.dependencies,
                        packages: buildConfig.packages,
                        env: finalEnv,
                        metadata: finalMetadata,
                        orgId,
                    });
                },
            });
            if (!options.json) {
                tui.success(`Created snapshot ${tui.bold(snapshot.snapshotId)}`);
                console.log('');
                tui.table([
                    {
                        Name: snapshot.name,
                        Description: snapshot.description ?? '-',
                        Runtime: buildConfig.runtime,
                        Tag: snapshot.tag ?? 'latest',
                        Size: tui.formatBytes(snapshot.sizeBytes),
                        Files: snapshot.fileCount.toFixed(),
                        Created: snapshot.createdAt,
                    },
                ], ['Name', 'Description', 'Runtime', 'Tag', 'Size', 'Files', 'Created'], { layout: 'vertical', padStart: '  ' });
                if (buildConfig.dependencies && buildConfig.dependencies.length > 0) {
                    console.log('');
                    tui.header('Dependencies');
                    for (const dep of buildConfig.dependencies) {
                        console.log(`  ${tui.muted('•')} ${dep}`);
                    }
                }
                if (buildConfig.packages && buildConfig.packages.length > 0) {
                    console.log('');
                    tui.header('Packages (npm/bun)');
                    for (const pkg of buildConfig.packages) {
                        console.log(`  ${tui.muted('•')} ${pkg}`);
                    }
                }
                if (finalEnv && Object.keys(finalEnv).length > 0) {
                    console.log('');
                    tui.header('Environment');
                    for (const [envKey, envValue] of Object.entries(finalEnv)) {
                        console.log(`  ${tui.muted('•')} ${envKey}=${tui.maskSecret(envValue)}`);
                    }
                }
                if (finalMetadata && Object.keys(finalMetadata).length > 0) {
                    console.log('');
                    tui.header('Metadata');
                    for (const key of Object.keys(finalMetadata)) {
                        console.log(`  ${tui.muted('•')} ${key}=${finalMetadata[key]}`);
                    }
                }
                if (snapshot.files && snapshot.files.length > 0) {
                    console.log('');
                    tui.header('Files');
                    printFileTree(snapshot.files);
                }
            }
            return {
                snapshotId: snapshot.snapshotId,
                name: snapshot.name,
                tag: snapshot.tag ?? undefined,
                runtime: buildConfig.runtime,
                sizeBytes: snapshot.sizeBytes,
                fileCount: snapshot.fileCount,
                createdAt: snapshot.createdAt,
                userMetadata: snapshot.userMetadata ?? undefined,
            };
        }
        finally {
            try {
                await rm(tempDir, { recursive: true, force: true });
            }
            catch {
                // Ignore cleanup errors
            }
        }
    },
});
export default buildSubcommand;
//# sourceMappingURL=build.js.map