import { GitError, runGitRaw } from './client.js';
const OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
const RESPONSE_OVERHEAD_BYTES = 16 * 1024 * 1024;
/**
 * Object ids are written to a newline-delimited stdin protocol. Reject
 * anything other than a full SHA-1 or SHA-256 id before it can become another
 * request. Requiring the complete id also keeps the returned map keyed by the
 * exact caller-owned identifier instead of Git's expansion of an abbreviation.
 */
export function assertGitObjectId(oid) {
    if (!OBJECT_ID.test(oid))
        throw new GitError('bad-ref', `Invalid object id "${oid}".`);
}
async function runObjectBatch(args, input, maxBuffer, options) {
    options.onCommandStart?.();
    const result = await runGitRaw(args, {
        repoRoot: options.repoRoot,
        signal: options.signal,
        input,
        maxBuffer,
        physicalObjects: options.physicalObjects ?? true,
    });
    options.onBytesRead?.(result.stdout.length);
    return result.stdout;
}
/** Read object type and size without materializing object bodies. */
export async function readGitObjectHeaders(oids, options) {
    const headers = new Map();
    if (oids.length === 0)
        return headers;
    for (const oid of oids)
        assertGitObjectId(oid);
    const output = await runObjectBatch(['cat-file', '--batch-check', '--buffer'], `${oids.join('\n')}\n`, undefined, options);
    for (const line of output.toString('utf8').split('\n')) {
        if (!line.trim())
            continue;
        const parts = line.split(' ');
        if (parts[1] === 'missing') {
            headers.set(parts[0], { oid: parts[0], type: 'missing', size: 0, missing: true });
            continue;
        }
        const size = Number(parts[2]);
        if (!parts[0] || !parts[1] || !Number.isSafeInteger(size) || size < 0) {
            throw new GitError('git-failed', 'Git emitted an invalid object header.');
        }
        headers.set(parts[0], { oid: parts[0], type: parts[1], size, missing: false });
    }
    for (const oid of oids) {
        if (!headers.has(oid))
            throw new GitError('git-failed', 'Git omitted an object header from its batch response.');
    }
    return headers;
}
/**
 * Read exact object bodies in byte-bounded batches. Reads are keyed by OID, so
 * duplicate requests are materialized once while callers retain occurrence
 * semantics separately.
 */
export async function readGitObjects(oids, headers, options) {
    const contents = new Map();
    if (oids.length === 0)
        return contents;
    if (!Number.isSafeInteger(options.maxBatchBytes) || options.maxBatchBytes <= 0) {
        throw new GitError('git-failed', 'Object batch byte limit must be a positive safe integer.');
    }
    let batch = [];
    let batchBytes = 0;
    const flush = async () => {
        if (batch.length === 0)
            return;
        const output = await runObjectBatch(['cat-file', '--batch', '--buffer'], `${batch.join('\n')}\n`, batchBytes + RESPONSE_OVERHEAD_BYTES, options);
        parseGitBatchStream(output, contents);
        for (const oid of batch) {
            if (!contents.has(oid))
                throw new GitError('git-failed', 'Git omitted an object body from its batch response.');
        }
        batch = [];
        batchBytes = 0;
    };
    for (const oid of oids) {
        assertGitObjectId(oid);
        const header = headers.get(oid);
        if (!header || header.missing)
            continue;
        // The batch limit bounds per-process transfer, not object size: an object
        // the caller's own size bounds admitted still gets read, alone in its own
        // batch, when it exceeds the batch limit by itself.
        if (batchBytes > 0 && batchBytes + header.size > options.maxBatchBytes)
            await flush();
        batch.push(oid);
        batchBytes += header.size;
        if (batchBytes > options.maxBatchBytes)
            await flush();
    }
    await flush();
    return contents;
}
/** Parse Git's length-prefixed `cat-file --batch` response without delimiters in content. */
export function parseGitBatchStream(buffer, into) {
    let offset = 0;
    while (offset < buffer.length) {
        const newline = buffer.indexOf(0x0a, offset);
        if (newline === -1)
            throw new GitError('git-failed', 'Git emitted a truncated object header.');
        const header = buffer.toString('utf8', offset, newline);
        const parts = header.split(' ');
        if (parts.length === 2 && parts[1] === 'missing') {
            offset = newline + 1;
            continue;
        }
        if (parts.length !== 3 || !OBJECT_ID.test(parts[0]) || !parts[1]) {
            throw new GitError('git-failed', 'Git emitted an invalid object body header.');
        }
        const size = Number(parts[2]);
        const start = newline + 1;
        const end = start + size;
        if (!Number.isSafeInteger(size) || size < 0 || end > buffer.length) {
            throw new GitError('git-failed', 'Git emitted a truncated object body.');
        }
        into.set(parts[0], buffer.subarray(start, end));
        if (buffer[end] !== 0x0a)
            throw new GitError('git-failed', 'Git omitted an object body terminator.');
        offset = end + 1;
    }
}
//# sourceMappingURL=objects.js.map