import { z } from 'zod';
import { GitError, runGit, runGitRaw } from './client.js';
const OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
const RECORD_SEPARATOR = '\u001e';
const UNIT_SEPARATOR = '\u001f';
const METADATA_BATCH_SIZE = 128;
export const GitDagCommitSchema = z
    .object({
    oid: z.string().regex(OBJECT_ID),
    parents: z.array(z.string().regex(OBJECT_ID)),
    author: z
        .object({
        name: z.string(),
        email: z.string(),
        time: z.string().min(1),
    })
        .strict(),
    committedAt: z.string().min(1),
    order: z.number().int().nonnegative(),
})
    .strict();
export const GitDagHistorySchema = z
    .object({
    requestedRef: z.string(),
    resolvedHead: z.string().regex(OBJECT_ID),
    resolvedSince: z.string().regex(OBJECT_ID).optional(),
    objectFormat: z.enum(['sha1', 'sha256']),
    shallow: z.boolean(),
    truncated: z.boolean(),
    commits: z.array(GitDagCommitSchema),
    emitCommits: z.instanceof((Set)),
})
    .strict();
function assertSafeRef(ref) {
    if (!ref || ref.startsWith('-') || ref.includes('\0')) {
        throw new GitError('bad-ref', `Invalid Git ref "${ref}".`);
    }
}
async function resolveCommit(ref, options) {
    assertSafeRef(ref);
    const result = await runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
        repoRoot: options.repoRoot,
        signal: options.signal,
        allowExitCodes: [1],
        physicalObjects: true,
    });
    const oid = result.stdout.trim();
    if (result.exitCode !== 0 || !OBJECT_ID.test(oid)) {
        throw new GitError('bad-ref', `The requested ref "${ref}" does not resolve to a commit.`);
    }
    return oid;
}
function parseTopology(text) {
    const records = [];
    for (const line of text.split('\n')) {
        if (!line.trim())
            continue;
        const [oid, ...parents] = line.trim().split(/\s+/);
        if (!oid || !OBJECT_ID.test(oid) || parents.some(parent => !OBJECT_ID.test(parent))) {
            throw new GitError('git-failed', 'Git emitted malformed DAG topology.');
        }
        records.push({ oid, parents });
    }
    return records;
}
function parseMetadata(text, expected) {
    const metadata = new Map();
    for (const raw of text.split(RECORD_SEPARATOR)) {
        if (!raw)
            continue;
        const fields = raw.replace(/^\r?\n/, '').split(UNIT_SEPARATOR);
        if (fields.length !== 5)
            throw new GitError('git-failed', 'Git emitted malformed DAG commit metadata.');
        const [oid, name, email, authoredAt, committedAtRaw] = fields;
        const committedAt = committedAtRaw?.trimEnd();
        if (!oid || !expected.has(oid) || !name || email === undefined || !authoredAt || !committedAt) {
            throw new GitError('git-failed', 'Git emitted incomplete DAG commit metadata.');
        }
        metadata.set(oid, {
            author: { name, email, time: authoredAt },
            committedAt,
        });
    }
    return metadata;
}
async function readMetadata(commits, options) {
    const metadata = new Map();
    const format = `${RECORD_SEPARATOR}%H${UNIT_SEPARATOR}%an${UNIT_SEPARATOR}%ae${UNIT_SEPARATOR}%aI${UNIT_SEPARATOR}%cI`;
    for (let start = 0; start < commits.length; start += METADATA_BATCH_SIZE) {
        const batch = commits.slice(start, start + METADATA_BATCH_SIZE).map(commit => commit.oid);
        const expected = new Set(batch);
        const result = await runGitRaw([
            'show',
            '--no-walk=unsorted',
            '--no-patch',
            '--no-notes',
            '--no-show-signature',
            `--format=format:${format}`,
            ...batch,
        ], {
            repoRoot: options.repoRoot,
            signal: options.signal,
            maxBuffer: 16 * 1024 * 1024,
            physicalObjects: true,
        });
        for (const [oid, value] of parseMetadata(result.stdout.toString('utf8'), expected))
            metadata.set(oid, value);
    }
    if (metadata.size !== commits.length)
        throw new GitError('git-failed', 'Git omitted DAG commit metadata.');
    return metadata;
}
async function commitsAfter(resolvedHead, resolvedSince, options) {
    const result = await runGit(['rev-list', resolvedHead, `^${resolvedSince}`], {
        repoRoot: options.repoRoot,
        signal: options.signal,
        physicalObjects: true,
    });
    const commits = new Set();
    for (const line of result.stdout.split('\n')) {
        const oid = line.trim();
        if (!oid)
            continue;
        if (!OBJECT_ID.test(oid))
            throw new GitError('git-failed', 'Git emitted an invalid incremental commit id.');
        commits.add(oid);
    }
    return commits;
}
/**
 * Read the newest bounded full-DAG window ending at one immutable commit.
 *
 * Git walks newest-first before applying `--max-count`; reversing that result
 * yields parents-before-children order inside the retained window while always
 * keeping the requested pin. Parents outside a truncated window remain named
 * on the boundary commit, so higher layers can report provisional coverage.
 */
export async function readGitDagHistory(options) {
    if (!Number.isSafeInteger(options.maxCommits) || options.maxCommits <= 0) {
        throw new GitError('git-failed', 'maxCommits must be a positive safe integer.');
    }
    const resolvedHead = await resolveCommit(options.ref, options);
    const resolvedSince = options.since ? await resolveCommit(options.since, options) : undefined;
    if (resolvedSince) {
        const ancestor = await runGit(['merge-base', '--is-ancestor', resolvedSince, resolvedHead], {
            repoRoot: options.repoRoot,
            signal: options.signal,
            allowExitCodes: [1],
            physicalObjects: true,
        });
        if (ancestor.exitCode !== 0) {
            throw new GitError('bad-ref', `Incremental ref ${resolvedSince} is not an ancestor of ${resolvedHead}.`);
        }
    }
    const [topologyResult, objectFormatResult, shallowResult] = await Promise.all([
        runGit(['rev-list', '--topo-order', '--parents', `--max-count=${options.maxCommits + 1}`, resolvedHead], {
            repoRoot: options.repoRoot,
            signal: options.signal,
            physicalObjects: true,
        }),
        runGit(['rev-parse', '--show-object-format=storage'], {
            repoRoot: options.repoRoot,
            signal: options.signal,
            physicalObjects: true,
        }).catch(() => ({ stdout: resolvedHead.length === 64 ? 'sha256\n' : 'sha1\n' })),
        runGit(['rev-parse', '--is-shallow-repository'], {
            repoRoot: options.repoRoot,
            signal: options.signal,
            physicalObjects: true,
        }),
    ]);
    const newestFirst = parseTopology(topologyResult.stdout);
    const truncated = newestFirst.length > options.maxCommits;
    if (truncated)
        newestFirst.splice(options.maxCommits);
    const topology = newestFirst.reverse();
    const metadata = await readMetadata(topology, options);
    const commits = topology.map((record, order) => {
        const details = metadata.get(record.oid);
        if (!details)
            throw new GitError('git-failed', `Missing metadata for commit ${record.oid}.`);
        return { ...record, ...details, order };
    });
    if (resolvedSince && !commits.some(commit => commit.oid === resolvedSince) && resolvedSince !== resolvedHead) {
        throw new GitError('bad-ref', `Incremental ref ${resolvedSince} falls outside the retained ${options.maxCommits}-commit window; raise maxCommits.`);
    }
    const emitCommits = resolvedSince
        ? await commitsAfter(resolvedHead, resolvedSince, options)
        : new Set(commits.map(commit => commit.oid));
    const objectFormatText = objectFormatResult.stdout.trim();
    const objectFormat = objectFormatText === 'sha256' ? 'sha256' : 'sha1';
    return {
        requestedRef: options.ref,
        resolvedHead,
        ...(resolvedSince ? { resolvedSince } : {}),
        objectFormat,
        shallow: shallowResult.stdout.trim() === 'true',
        truncated,
        commits,
        emitCommits,
    };
}
//# sourceMappingURL=dag.js.map