"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateReleaseArchiveExecution = validateReleaseArchiveExecution;
exports.validateReleaseExecutable = validateReleaseExecutable;
exports.runReleaseValidationCommand = runReleaseValidationCommand;
exports.captureReleaseValidationStdout = captureReleaseValidationStdout;
exports.extractReleaseArchive = extractReleaseArchive;
exports.extractTarGzipArchive = extractTarGzipArchive;
exports.extractStoredZipArchive = extractStoredZipArchive;
exports.parseAboutJson = parseAboutJson;
exports.readTarString = readTarString;
exports.resolveArchiveDestination = resolveArchiveDestination;
exports.summarizeCommandOutput = summarizeCommandOutput;
exports.toReleaseValidationPath = toReleaseValidationPath;
// Responsibility-focused extraction from index-local-validation.ts.
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const node_child_process_1 = require("node:child_process");
const node_zlib_1 = __importDefault(require("node:zlib"));
const cli_constants_1 = require("../../../lib/config/cli-constants");
const errors_1 = require("../../../lib/errors");
const output_safety_1 = require("../../../lib/output/output-safety");
function validateReleaseArchiveExecution(options) {
    const tempRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(options.versionDirectory, 'archive-validation-'));
    const requiredFiles = [
        'dist/cli/index.js',
        'package.json',
        '.day-shift-release.json',
        cli_constants_1.RELEASE_INSTALLATION_README_FILENAME,
        cli_constants_1.RELEASE_NOTES_FILENAME,
        ...options.target.launcherPaths,
    ];
    let result = null;
    try {
        extractReleaseArchive({
            archivePath: node_path_1.default.join(options.repositoryRoot, ...options.target.archivePath.split('/')),
            destinationDirectory: tempRoot,
            target: options.target.target,
        });
        const roots = node_fs_1.default
            .readdirSync(tempRoot, { withFileTypes: true })
            .filter((entry) => entry.isDirectory())
            .map((entry) => entry.name)
            .sort();
        if (roots.length !== 1 || roots[0] !== options.target.rootDirectoryName) {
            throw (0, errors_1.createUsageError)('Release archive validation requires exactly one expected payload root directory.', [
                'Reason: archive-unpack-root-mismatch',
                `Archive: ${options.target.archivePath}`,
                `Expected root: ${options.target.rootDirectoryName}`,
                `Found roots: ${roots.length === 0 ? '(none)' : roots.join(', ')}`,
                'Recovery: regenerate release archives with the expected root directory layout.',
                'Exit behavior: exits before successful-release state is recorded.',
            ]);
        }
        const artifactRoot = node_path_1.default.join(tempRoot, options.target.rootDirectoryName);
        const missingFiles = requiredFiles.filter((filePath) => !node_fs_1.default.existsSync(node_path_1.default.join(artifactRoot, ...filePath.split('/'))));
        if (missingFiles.length > 0) {
            throw (0, errors_1.createUsageError)('Release archive validation requires expected runtime files after unpacking.', [
                'Reason: archive-unpack-layout-missing-files',
                `Archive: ${options.target.archivePath}`,
                `Missing files: ${missingFiles.join(', ')}`,
                'Recovery: regenerate release archives with the expected runtime layout.',
                'Exit behavior: exits before successful-release state is recorded.',
            ]);
        }
        result = {
            status: 'passed',
            target: options.target.target,
            archivePath: options.target.archivePath,
            rootDirectoryName: options.target.rootDirectoryName,
            temporaryRoot: tempRoot,
            requiredFiles,
            missingFiles: [],
            execution: options.target.target === 'macos-linux'
                ? validateReleaseExecutable({
                    executable: node_path_1.default.join(artifactRoot, 'bin', options.commandName),
                    artifactRoot,
                    repositoryRoot: options.repositoryRoot,
                    versionDirectory: options.versionDirectory,
                    version: options.version,
                    commandName: options.commandName,
                })
                : null,
            executionSkippedReason: options.target.target === 'macos-linux'
                ? null
                : 'Windows archive execution requires a Windows command shell; layout is validated on this runner.',
        };
    }
    finally {
        node_fs_1.default.rmSync(tempRoot, { recursive: true, force: true });
    }
    if (result === null) {
        throw (0, errors_1.createUsageError)('Release archive validation did not produce execution evidence.', [
            'Reason: archive-validation-missing-result',
            `Archive: ${options.target.archivePath}`,
            'Recovery: rerun release build after regenerating archive validation.',
            'Exit behavior: exits before successful-release state is recorded.',
        ]);
    }
    return {
        ...result,
        temporaryCleaned: !node_fs_1.default.existsSync(tempRoot),
    };
}
function validateReleaseExecutable(options) {
    const validationScratchRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(options.versionDirectory, 'validation-scratch-'));
    const isolatedEnvironment = {
        stateDir: node_fs_1.default.mkdtempSync(node_path_1.default.join(validationScratchRoot, 'day-shift-release-validation-state-')),
        configHome: node_fs_1.default.mkdtempSync(node_path_1.default.join(validationScratchRoot, 'day-shift-release-validation-config-')),
        licenseFile: node_path_1.default.join(node_fs_1.default.mkdtempSync(node_path_1.default.join(validationScratchRoot, 'day-shift-release-validation-license-')), 'license.json'),
    };
    const commandSpecs = [
        { name: 'help', args: ['--help'], format: 'human' },
        { name: 'version', args: ['--version'], format: 'human' },
        { name: 'about', args: ['about'], format: 'human' },
        { name: 'about-json', args: ['about', '--format', 'json'], format: 'json' },
    ];
    const outputs = new Map();
    let usedNodeEntrypointFallback = false;
    let validation = null;
    try {
        const commands = commandSpecs.map((spec) => {
            const result = runReleaseValidationCommand({
                executable: options.executable,
                artifactRoot: options.artifactRoot,
                args: spec.args,
                isolatedEnvironment,
                validationScratchRoot,
            });
            usedNodeEntrypointFallback ||= result.usedNodeEntrypointFallback;
            outputs.set(spec.name, result.stdout);
            return {
                name: spec.name,
                args: [...spec.args],
                format: spec.format,
                status: 'passed',
                exitCode: 0,
                stdoutSummary: summarizeCommandOutput(result.stdout),
            };
        });
        const versionOutput = outputs.get('version')?.trim() ?? '';
        const aboutOutput = outputs.get('about') ?? '';
        const aboutJsonOutput = outputs.get('about-json') ?? '';
        const aboutJson = parseAboutJson(aboutJsonOutput);
        const acceptedArtifactRoots = new Set([
            options.artifactRoot,
            (0, output_safety_1.sanitizeOutputText)(options.artifactRoot, { repositoryRoot: options.repositoryRoot }),
            (0, output_safety_1.sanitizeOutputText)(options.artifactRoot, { repositoryRoot: options.artifactRoot }),
        ]);
        const sourceCheckoutPathPresent = [aboutOutput, aboutJsonOutput].some((output) => output.split(options.artifactRoot).join('').includes(options.repositoryRoot));
        if (versionOutput !== options.version) {
            throw (0, errors_1.createUsageError)('Release validation expected packaged --version output to match release metadata.', [
                'Reason: release-runtime-version-mismatch',
                `Executable: ${options.executable}`,
                `Expected version: ${options.version}`,
                `Received version: ${versionOutput}`,
                'Recovery: ensure packaged runtime metadata is read from the release artifact.',
                'Exit behavior: exits before successful-release state is recorded.',
            ]);
        }
        if (aboutJson.status !== 'ok' ||
            aboutJson.command !== 'about' ||
            aboutJson.data?.release?.version !== options.version ||
            aboutJson.data?.release?.installedCommandName !== options.commandName ||
            typeof aboutJson.data?.runtime?.artifactRoot !== 'string' ||
            !acceptedArtifactRoots.has(aboutJson.data.runtime.artifactRoot) ||
            aboutJson.data?.runtime?.entrypoint !== 'dist/cli/index.js') {
            throw (0, errors_1.createUsageError)('Release validation expected packaged about JSON to report artifact-relative runtime metadata.', [
                'Reason: release-runtime-diagnostic-mismatch',
                `Executable: ${options.executable}`,
                'Recovery: ensure packaged about output reads release metadata and reports artifact-relative runtime paths.',
                'Exit behavior: exits before successful-release state is recorded.',
            ]);
        }
        if (!aboutOutput.includes(`Release version: ${options.version}`) ||
            !aboutOutput.includes(`Installed command: ${options.commandName}`) ||
            !aboutOutput.includes('Entrypoint: dist/cli/index.js') ||
            sourceCheckoutPathPresent) {
            throw (0, errors_1.createUsageError)('Release validation expected packaged about text to report artifact runtime metadata without source checkout paths.', [
                'Reason: release-runtime-diagnostic-text-mismatch',
                `Executable: ${options.executable}`,
                'Recovery: ensure packaged about text uses release metadata and artifact-relative paths.',
                'Exit behavior: exits before successful-release state is recorded.',
            ]);
        }
        validation = {
            status: 'passed',
            executable: toReleaseValidationPath(options.executable, options.repositoryRoot),
            invocationMethod: usedNodeEntrypointFallback ? 'posix-shell-with-node-entrypoint-fallback' : 'posix-shell',
            artifactRoot: toReleaseValidationPath(options.artifactRoot, options.repositoryRoot),
            commands,
            diagnostics: {
                status: 'passed',
                version: options.version,
                installedCommandName: options.commandName,
                artifactRoot: toReleaseValidationPath(options.artifactRoot, options.repositoryRoot),
                entrypoint: aboutJson.data.runtime.entrypoint,
                commandRegistry: aboutJson.data.runtime.commandRegistry,
                sourceCheckoutPathPresent,
                jsonOutput: true,
                humanOutput: true,
            },
            validationScratchRoot: toReleaseValidationPath(validationScratchRoot, options.repositoryRoot),
            isolatedEnvironment: {
                stateDir: toReleaseValidationPath(isolatedEnvironment.stateDir, options.repositoryRoot),
                configHome: toReleaseValidationPath(isolatedEnvironment.configHome, options.repositoryRoot),
                licenseFile: toReleaseValidationPath(isolatedEnvironment.licenseFile, options.repositoryRoot),
            },
        };
    }
    finally {
        node_fs_1.default.rmSync(validationScratchRoot, { recursive: true, force: true });
    }
    if (validation === null) {
        throw (0, errors_1.createUsageError)('Release validation did not produce packaged command evidence.', [
            'Reason: release-runtime-validation-missing-result',
            `Executable: ${options.executable}`,
            'Recovery: rerun release check after regenerating the staged release payload.',
            'Exit behavior: exits before successful-release state is recorded.',
        ]);
    }
    return {
        ...validation,
        validationScratchCleaned: !node_fs_1.default.existsSync(validationScratchRoot),
    };
}
function runReleaseValidationCommand(options) {
    const env = {
        ...process.env,
        DAY_SHIFT_REPOSITORY_ROOT: options.artifactRoot,
        DAY_SHIFT_STATE_DIR: options.isolatedEnvironment.stateDir,
        XDG_CONFIG_HOME: options.isolatedEnvironment.configHome,
        DAY_SHIFT_LICENSE_FILE: options.isolatedEnvironment.licenseFile,
    };
    const stdout = captureReleaseValidationStdout('sh', [options.executable, ...options.args], {
        cwd: options.artifactRoot,
        env,
        validationScratchRoot: options.validationScratchRoot,
    });
    if (stdout.trim().length > 0) {
        return {
            stdout,
            usedNodeEntrypointFallback: false,
        };
    }
    return {
        stdout: captureReleaseValidationStdout(process.execPath, [node_path_1.default.join(options.artifactRoot, 'dist', 'cli', 'index.js'), ...options.args], {
            cwd: options.artifactRoot,
            env,
            validationScratchRoot: options.validationScratchRoot,
        }),
        usedNodeEntrypointFallback: true,
    };
}
function captureReleaseValidationStdout(command, args, options) {
    const outputDirectory = node_fs_1.default.mkdtempSync(node_path_1.default.join(options.validationScratchRoot, 'day-shift-release-validation-stdout-'));
    const outputPath = node_path_1.default.join(outputDirectory, 'stdout.txt');
    const outputFd = node_fs_1.default.openSync(outputPath, 'w');
    try {
        const result = (0, node_child_process_1.spawnSync)(command, args, {
            cwd: options.cwd,
            encoding: 'utf8',
            env: options.env,
            stdio: ['ignore', outputFd, 'pipe'],
        });
        if (result.error !== undefined) {
            throw result.error;
        }
        if (result.status !== 0) {
            throw (0, errors_1.createUsageError)('Release validation command failed.', [
                'Reason: release-validation-command-failed',
                `Command: ${[command, ...args].join(' ')}`,
                `Exit code: ${result.status ?? 'unknown'}`,
                `Stderr: ${typeof result.stderr === 'string' && result.stderr.trim().length > 0 ? summarizeCommandOutput(result.stderr) : '(empty)'}`,
                'Recovery: inspect the packaged release command and rerun release check.',
                'Exit behavior: exits before successful-release state is recorded.',
            ]);
        }
        node_fs_1.default.closeSync(outputFd);
        return node_fs_1.default.readFileSync(outputPath, 'utf8');
    }
    finally {
        try {
            node_fs_1.default.closeSync(outputFd);
        }
        catch {
            // The descriptor may already be closed after a successful command.
        }
        node_fs_1.default.rmSync(outputDirectory, { recursive: true, force: true });
    }
}
function extractReleaseArchive(options) {
    if (options.target === 'macos-linux') {
        extractTarGzipArchive(options.archivePath, options.destinationDirectory);
        return;
    }
    extractStoredZipArchive(options.archivePath, options.destinationDirectory);
}
function extractTarGzipArchive(archivePath, destinationDirectory) {
    const tar = node_zlib_1.default.gunzipSync(node_fs_1.default.readFileSync(archivePath));
    let offset = 0;
    while (offset + 512 <= tar.length) {
        const header = tar.subarray(offset, offset + 512);
        if (header.every((byte) => byte === 0)) {
            break;
        }
        const name = readTarString(header, 0, 100);
        const prefix = readTarString(header, 345, 155);
        const entryName = prefix.length > 0 ? `${prefix}/${name}` : name;
        const mode = Number.parseInt(readTarString(header, 100, 8).trim() || '644', 8);
        const size = Number.parseInt(readTarString(header, 124, 12).trim() || '0', 8);
        const fileStart = offset + 512;
        const fileEnd = fileStart + size;
        const destinationPath = resolveArchiveDestination(destinationDirectory, entryName);
        node_fs_1.default.mkdirSync(node_path_1.default.dirname(destinationPath), { recursive: true });
        node_fs_1.default.writeFileSync(destinationPath, tar.subarray(fileStart, fileEnd));
        node_fs_1.default.chmodSync(destinationPath, mode);
        offset = fileEnd + ((512 - (size % 512)) % 512);
    }
}
function extractStoredZipArchive(archivePath, destinationDirectory) {
    const zip = node_fs_1.default.readFileSync(archivePath);
    let offset = 0;
    while (offset + 30 <= zip.length && zip.readUInt32LE(offset) === 0x04034b50) {
        const compressionMethod = zip.readUInt16LE(offset + 8);
        const compressedSize = zip.readUInt32LE(offset + 18);
        const uncompressedSize = zip.readUInt32LE(offset + 22);
        const fileNameLength = zip.readUInt16LE(offset + 26);
        const extraLength = zip.readUInt16LE(offset + 28);
        const nameStart = offset + 30;
        const dataStart = nameStart + fileNameLength + extraLength;
        const dataEnd = dataStart + compressedSize;
        const entryName = zip.subarray(nameStart, nameStart + fileNameLength).toString('utf8');
        if (compressionMethod !== 0 || compressedSize !== uncompressedSize) {
            throw (0, errors_1.createUsageError)('Release archive validation only supports stored ZIP entries from the deterministic archive writer.', [
                'Reason: unsupported-zip-compression',
                `Archive: ${archivePath}`,
                'Recovery: update archive validation when ZIP compression is introduced.',
                'Exit behavior: exits before successful-release state is recorded.',
            ]);
        }
        const destinationPath = resolveArchiveDestination(destinationDirectory, entryName);
        node_fs_1.default.mkdirSync(node_path_1.default.dirname(destinationPath), { recursive: true });
        node_fs_1.default.writeFileSync(destinationPath, zip.subarray(dataStart, dataEnd));
        offset = dataEnd;
    }
}
function parseAboutJson(output) {
    try {
        return JSON.parse(output);
    }
    catch {
        throw (0, errors_1.createUsageError)('Release validation expected about JSON output to be parseable.', [
            'Reason: release-runtime-about-json-unparseable',
            `Output: ${summarizeCommandOutput(output)}`,
            'Recovery: ensure packaged about output still supports --format json.',
            'Exit behavior: exits before successful-release state is recorded.',
        ]);
    }
}
function readTarString(buffer, offset, length) {
    const end = buffer.indexOf(0, offset);
    const sliceEnd = end === -1 || end > offset + length ? offset + length : end;
    return buffer.subarray(offset, sliceEnd).toString('utf8').trim();
}
function resolveArchiveDestination(root, entryName) {
    const destinationPath = node_path_1.default.resolve(root, ...entryName.split('/'));
    const resolvedRoot = node_path_1.default.resolve(root);
    if (destinationPath !== resolvedRoot && !destinationPath.startsWith(`${resolvedRoot}${node_path_1.default.sep}`)) {
        throw (0, errors_1.createUsageError)('Release archive validation rejected an unsafe archive entry path.', [
            'Reason: archive-entry-outside-validation-root',
            `Entry: ${entryName}`,
            'Recovery: regenerate release archives without absolute or parent-traversing entries.',
            'Exit behavior: exits before successful-release state is recorded.',
        ]);
    }
    return destinationPath;
}
function summarizeCommandOutput(output) {
    return output.replace(/\s+/g, ' ').trim().slice(0, 160);
}
function toReleaseValidationPath(value, repositoryRoot) {
    const relative = node_path_1.default.relative(repositoryRoot, value).split(node_path_1.default.sep).join('/');
    return relative.length > 0 && !relative.startsWith('..') ? relative : value;
}
