"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateVersionInstallSource = validateVersionInstallSource;
const node_crypto_1 = __importDefault(require("node:crypto"));
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const node_zlib_1 = __importDefault(require("node:zlib"));
const EXACT_STABLE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
function validateVersionInstallSource(options) {
    const normalizedSource = normalizeInstallSourcePath({
        sourcePath: options.sourcePath,
        env: options.env ?? process.env,
        platform: process.platform
    });
    if (!normalizedSource.ok) {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: "unknown",
            reason: "unsafe-windows-path",
            message: normalizedSource.message,
            details: normalizedSource.details
        });
    }
    const sourcePath = node_path_1.default.resolve(normalizedSource.sourcePath);
    const sourceKind = classifySource(sourcePath);
    if (!node_fs_1.default.existsSync(sourcePath)) {
        return failure({
            sourcePath,
            sourceKind,
            reason: "missing-source",
            message: "Version install source does not exist.",
            details: [`Source path: ${sourcePath}`]
        });
    }
    const resolved = resolvePackageRoot({
        sourcePath,
        sourceKind,
        stagingDirectory: options.stagingDirectory
    });
    if (!resolved.ok) {
        return resolved.failure;
    }
    return validatePackageRoot({
        sourceKind: resolved.sourceKind,
        sourcePath,
        packageRoot: resolved.packageRoot,
        assertedVersion: options.assertedVersion,
        checksumRecords: options.checksumRecords,
        nodeVersion: options.nodeVersion ?? process.versions.node
    });
}
function normalizeInstallSourcePath(options) {
    const sourcePath = options.sourcePath.trim();
    if (sourcePath.startsWith("\\\\") || sourcePath.startsWith("//")) {
        return {
            ok: false,
            message: "Windows network share install sources are not supported.",
            details: [
                `Source path: ${options.sourcePath}`,
                "Copy the package to a local filesystem path before installing."
            ]
        };
    }
    const driveMatch = /^([a-zA-Z]):[\\/](.*)$/.exec(sourcePath);
    if (driveMatch === null || options.platform === "win32") {
        return { ok: true, sourcePath };
    }
    const drive = driveMatch[1].toLowerCase();
    const remainder = driveMatch[2].replace(/[\\/]+/g, node_path_1.default.sep);
    const mountRoot = options.env.DAY_SHIFT_WSL_MOUNT_ROOT?.trim() || "/mnt";
    const mappedPath = node_path_1.default.join(mountRoot, drive, remainder);
    if (!node_fs_1.default.existsSync(mappedPath)) {
        return {
            ok: false,
            message: "Windows install source path could not be mapped to an existing local WSL path.",
            details: [
                `Source path: ${options.sourcePath}`,
                `Mapped path: ${mappedPath}`,
                "Only existing local drive-letter mappings are accepted."
            ]
        };
    }
    return { ok: true, sourcePath: mappedPath };
}
function resolvePackageRoot(options) {
    if (options.sourceKind === "unknown") {
        return {
            ok: false,
            failure: failure({
                sourcePath: options.sourcePath,
                sourceKind: "unknown",
                reason: "unsupported-source",
                message: "Version install source must be an unpacked package root, .tar.gz archive, or .zip archive.",
                details: [`Source path: ${options.sourcePath}`]
            })
        };
    }
    try {
        if (options.sourceKind === "directory") {
            return resolveDirectoryPackageRoot(options.sourcePath, options.sourceKind);
        }
        const archiveRoot = node_path_1.default.join(node_path_1.default.resolve(options.stagingDirectory), `${options.sourceKind}-${Date.now()}-${process.pid}`);
        node_fs_1.default.mkdirSync(archiveRoot, { recursive: true });
        if (options.sourceKind === "tar.gz") {
            extractTarGzipArchive(options.sourcePath, archiveRoot);
        }
        else {
            extractStoredZipArchive(options.sourcePath, archiveRoot);
        }
        return resolveDirectoryPackageRoot(archiveRoot, options.sourceKind);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        const reason = message.includes("unsafe")
            ? "unsafe-archive-entry"
            : "unsupported-archive";
        return {
            ok: false,
            failure: failure({
                sourcePath: options.sourcePath,
                sourceKind: options.sourceKind,
                reason,
                message: "Version install archive could not be extracted safely.",
                details: [message]
            })
        };
    }
}
function resolveDirectoryPackageRoot(directoryPath, sourceKind) {
    const directPackageJsonPath = node_path_1.default.join(directoryPath, "package.json");
    if (node_fs_1.default.existsSync(directPackageJsonPath) && node_fs_1.default.statSync(directPackageJsonPath).isFile()) {
        return { ok: true, sourceKind, packageRoot: directoryPath };
    }
    const entries = node_fs_1.default.readdirSync(directoryPath, { withFileTypes: true });
    const childPackageRoots = entries
        .filter((entry) => entry.isDirectory())
        .map((entry) => node_path_1.default.join(directoryPath, entry.name))
        .filter((entryPath) => node_fs_1.default.existsSync(node_path_1.default.join(entryPath, "package.json")));
    if (childPackageRoots.length === 1 && entries.every((entry) => entry.isDirectory())) {
        return { ok: true, sourceKind, packageRoot: childPackageRoots[0] };
    }
    return {
        ok: false,
        failure: failure({
            sourcePath: directoryPath,
            sourceKind,
            reason: "unsupported-layout",
            message: "Version install source must contain exactly one package root.",
            details: [`Package roots found: ${childPackageRoots.length}`]
        })
    };
}
function validatePackageRoot(options) {
    const packageJsonPath = node_path_1.default.join(options.packageRoot, "package.json");
    let packageJson;
    try {
        packageJson = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
    }
    catch (error) {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: options.sourceKind,
            reason: "malformed-package-json",
            message: "Package metadata is not valid JSON.",
            details: [error instanceof Error ? error.message : String(error)]
        });
    }
    if (packageJson.name !== "day-shift") {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: options.sourceKind,
            reason: "wrong-package-name",
            message: "Package metadata does not identify Day Shift.",
            details: [`Expected name: day-shift`, `Actual name: ${String(packageJson.name)}`]
        });
    }
    if (typeof packageJson.version !== "string" || !EXACT_STABLE_VERSION_PATTERN.test(packageJson.version)) {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: options.sourceKind,
            reason: "invalid-version",
            message: "Package version must be an exact stable semantic version.",
            details: [`Actual version: ${String(packageJson.version)}`]
        });
    }
    if (options.assertedVersion !== undefined && packageJson.version !== options.assertedVersion) {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: options.sourceKind,
            reason: "version-assertion-mismatch",
            message: "Asserted install version does not match package metadata.",
            details: [`Asserted version: ${options.assertedVersion}`, `Package version: ${packageJson.version}`]
        });
    }
    const commandPath = resolvePackageCommandPath(options.packageRoot, packageJson.bin);
    if (commandPath === null) {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: options.sourceKind,
            reason: "wrong-command-name",
            message: "Package metadata must expose the day-shift command.",
            details: ["Expected package.json bin.day-shift."]
        });
    }
    if (!node_fs_1.default.existsSync(commandPath) || !node_fs_1.default.statSync(commandPath).isFile()) {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: options.sourceKind,
            reason: "missing-command-file",
            message: "Package command file is missing.",
            details: [`Command path: ${commandPath}`]
        });
    }
    const runtimeRequirement = readNodeRuntimeRequirement(packageJson.engines);
    if (!isNodeRuntimeCompatible(runtimeRequirement, options.nodeVersion)) {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: options.sourceKind,
            reason: "unmet-runtime-requirement",
            message: "Current Node.js runtime does not satisfy the package requirement.",
            details: [
                `Required node: ${runtimeRequirement}`,
                `Current node: ${options.nodeVersion}`
            ]
        });
    }
    const checksumStatus = validateChecksums({
        packageRoot: options.packageRoot,
        checksumRecords: options.checksumRecords
    });
    if (checksumStatus !== "verified" && checksumStatus !== "not-provided") {
        return failure({
            sourcePath: options.sourcePath,
            sourceKind: options.sourceKind,
            reason: "checksum-mismatch",
            message: "Package checksum metadata does not match extracted files.",
            details: [checksumStatus]
        });
    }
    return {
        status: "valid",
        sourceKind: options.sourceKind,
        sourcePath: options.sourcePath,
        packageRoot: options.packageRoot,
        packageName: "day-shift",
        version: packageJson.version,
        commandName: "day-shift",
        commandPath,
        runtimeRequirement,
        checksumStatus
    };
}
function classifySource(sourcePath) {
    if (node_fs_1.default.existsSync(sourcePath) && node_fs_1.default.statSync(sourcePath).isDirectory()) {
        return "directory";
    }
    if (sourcePath.endsWith(".tar.gz")) {
        return "tar.gz";
    }
    if (sourcePath.endsWith(".zip")) {
        return "zip";
    }
    return "unknown";
}
function resolvePackageCommandPath(packageRoot, bin) {
    if (typeof bin === "object" && bin !== null && typeof bin["day-shift"] === "string") {
        return node_path_1.default.resolve(packageRoot, bin["day-shift"]);
    }
    return null;
}
function readNodeRuntimeRequirement(engines) {
    if (typeof engines !== "object" || engines === null) {
        return null;
    }
    const node = engines.node;
    return typeof node === "string" && node.trim() !== "" ? node.trim() : null;
}
function isNodeRuntimeCompatible(requirement, nodeVersion) {
    if (requirement === null) {
        return true;
    }
    const match = requirement.match(/^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
    if (match === null) {
        return false;
    }
    const required = [
        Number(match[1]),
        Number(match[2] ?? "0"),
        Number(match[3] ?? "0")
    ];
    const current = nodeVersion.split(".").map((part) => Number(part));
    for (let index = 0; index < 3; index += 1) {
        if ((current[index] ?? 0) > required[index]) {
            return true;
        }
        if ((current[index] ?? 0) < required[index]) {
            return false;
        }
    }
    return true;
}
function validateChecksums(options) {
    if (options.checksumRecords === undefined || options.checksumRecords.length === 0) {
        return "not-provided";
    }
    for (const record of options.checksumRecords) {
        const candidate = node_path_1.default.resolve(options.packageRoot, record.path);
        if (!isPathInside(options.packageRoot, candidate) || !node_fs_1.default.existsSync(candidate)) {
            return `Missing checksum target: ${record.path}`;
        }
        const actual = node_crypto_1.default.createHash("sha256")
            .update(node_fs_1.default.readFileSync(candidate))
            .digest("hex");
        if (actual !== record.sha256) {
            return `Checksum mismatch for ${record.path}`;
        }
    }
    return "verified";
}
function extractTarGzipArchive(archivePath, destinationDirectory) {
    const buffer = node_zlib_1.default.gunzipSync(node_fs_1.default.readFileSync(archivePath));
    let offset = 0;
    while (offset + 512 <= buffer.length) {
        const header = buffer.subarray(offset, offset + 512);
        if (header.every((byte) => byte === 0)) {
            return;
        }
        const name = readTarString(header.subarray(0, 100));
        const sizeText = readTarString(header.subarray(124, 136)).trim();
        const typeFlag = String.fromCharCode(header[156] || 48);
        const size = Number.parseInt(sizeText || "0", 8);
        const dataStart = offset + 512;
        const dataEnd = dataStart + size;
        const destinationPath = safeDestinationPath(destinationDirectory, name);
        if (typeFlag === "5") {
            node_fs_1.default.mkdirSync(destinationPath, { recursive: true });
        }
        else if (typeFlag === "0" || typeFlag === "\0") {
            node_fs_1.default.mkdirSync(node_path_1.default.dirname(destinationPath), { recursive: true });
            node_fs_1.default.writeFileSync(destinationPath, buffer.subarray(dataStart, dataEnd));
        }
        offset = dataStart + Math.ceil(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);
        if (compressionMethod !== 0 || compressedSize !== uncompressedSize) {
            throw new Error("unsupported zip compression");
        }
        const nameStart = offset + 30;
        const dataStart = nameStart + fileNameLength + extraLength;
        const dataEnd = dataStart + compressedSize;
        const entryName = zip.subarray(nameStart, nameStart + fileNameLength).toString("utf8");
        if (entryName.endsWith("/")) {
            node_fs_1.default.mkdirSync(safeDestinationPath(destinationDirectory, entryName), { recursive: true });
        }
        else {
            const destinationPath = safeDestinationPath(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 safeDestinationPath(root, entryName) {
    const destinationPath = node_path_1.default.resolve(root, entryName);
    if (!isPathInside(root, destinationPath)) {
        throw new Error(`unsafe archive entry: ${entryName}`);
    }
    return destinationPath;
}
function isPathInside(root, candidate) {
    const relative = node_path_1.default.relative(node_path_1.default.resolve(root), node_path_1.default.resolve(candidate));
    return relative === "" || (!relative.startsWith("..") && !node_path_1.default.isAbsolute(relative));
}
function readTarString(buffer) {
    const zeroIndex = buffer.indexOf(0);
    return buffer.subarray(0, zeroIndex === -1 ? buffer.length : zeroIndex).toString("utf8");
}
function failure(options) {
    return {
        status: "invalid",
        ...options
    };
}
