import GraphModelMigrationLoweredPatchPlan from "../../../../src/domain/migrations/GraphModelMigrationLoweredPatchPlan.js";
import GraphModelMigrationNotice from "../../../../src/domain/migrations/GraphModelMigrationNotice.js";
import GraphModelMigrationScratchRef from "../../../../src/domain/migrations/GraphModelMigrationScratchRef.js";
import GraphModelMigrationScratchWrittenPatch from "../../../../src/domain/migrations/GraphModelMigrationScratchWrittenPatch.js";
import GraphModelMigrationScratchWriteResult from "../../../../src/domain/migrations/GraphModelMigrationScratchWriteResult.js";
import { runMigrationGit } from "./GitMigrationCommandRunner.js";
const ZERO_OID = '0000000000000000000000000000000000000000';
const OPERATION_TREE_PATH = 'migration-operation.txt';
export class GraphModelMigrationScratchWriterError extends Error {
    constructor(message) {
        super(message);
        this.name = 'GraphModelMigrationScratchWriterError';
    }
}
/** Writes lowered graph-model migration operations to an explicit scratch ref. */
export async function writeGraphModelMigrationScratchHistory(options) {
    const repositoryPath = requireNonEmptyString(options.repositoryPath, 'repositoryPath');
    const scratchRefNotice = GraphModelMigrationScratchRef.validateRefName(options.scratchRefName);
    if (scratchRefNotice !== null) {
        return blockedResult(null, scratchRefNotice);
    }
    const scratchRef = new GraphModelMigrationScratchRef({ refName: requireScratchRefName(options.scratchRefName) });
    const patchPlan = requirePatchPlan(options.patchPlan);
    const gitRefNotice = await validateGitRefName(repositoryPath, scratchRef);
    if (gitRefNotice !== null) {
        return blockedResult(scratchRef, gitRefNotice);
    }
    let currentHead = await gitTextOrNull(repositoryPath, ['show-ref', '--verify', '--hash', scratchRef.refName]);
    const writtenPatches = [];
    let sequence = 0;
    for (const operation of patchPlan.operations) {
        const commitId = await writeOperationCommit({
            repositoryPath,
            patchPlan,
            operation,
            sequence,
            parentHead: currentHead,
        });
        await advanceScratchRef(repositoryPath, scratchRef, commitId, currentHead);
        currentHead = commitId;
        writtenPatches.push(new GraphModelMigrationScratchWrittenPatch({
            commitId,
            operation,
            sequence,
        }));
        sequence += 1;
    }
    return new GraphModelMigrationScratchWriteResult({
        scratchRef,
        scratchHead: currentHead,
        writtenPatches,
        warnings: [],
        fatalErrors: [],
    });
}
async function writeOperationCommit(options) {
    const payload = formatOperationPayload(options.patchPlan, options.operation, options.sequence);
    const blobOid = await gitTextWithInput(options.repositoryPath, ['hash-object', '-w', '--stdin'], payload);
    const treeOid = await gitTextWithInput(options.repositoryPath, ['mktree'], `100644 blob ${blobOid}\t${OPERATION_TREE_PATH}\n`);
    const parentArgs = options.parentHead === null ? [] : ['-p', options.parentHead];
    return await gitTextWithInput(options.repositoryPath, ['commit-tree', treeOid, ...parentArgs], formatCommitMessage(options.patchPlan, options.operation, options.sequence), true);
}
async function advanceScratchRef(repositoryPath, scratchRef, commitId, expectedHead) {
    const expected = expectedHead ?? ZERO_OID;
    await gitText(repositoryPath, ['update-ref', scratchRef.refName, commitId, expected]);
}
async function validateGitRefName(repositoryPath, scratchRef) {
    const result = await runMigrationGit(repositoryPath, ['check-ref-format', scratchRef.refName], null);
    if (result.ok()) {
        return null;
    }
    return GraphModelMigrationNotice.fatal('E_INVALID_SCRATCH_REF', `git rejected scratch migration ref ${scratchRef.refName}`);
}
function blockedResult(scratchRef, fatalError) {
    return new GraphModelMigrationScratchWriteResult({
        scratchRef,
        scratchHead: null,
        writtenPatches: [],
        warnings: [],
        fatalErrors: [fatalError],
    });
}
function formatOperationPayload(patchPlan, operation, sequence) {
    return [
        'git-warp-v18-migration-operation-v1',
        `sequence ${sequence}`,
        `kind ${operation.kind}`,
        `source-basis-utf8-hex ${utf8Hex(patchPlan.sourceBasis.toKey())}`,
        `target-basis-utf8-hex ${utf8Hex(patchPlan.targetBasis.toKey())}`,
        `source-key-utf8-hex ${utf8Hex(operation.sourceKey)}`,
        `target-key-utf8-hex ${utf8Hex(operation.targetKey)}`,
        `operation-key-utf8-hex ${utf8Hex(operation.toKey())}`,
        '',
    ].join('\n');
}
function formatCommitMessage(patchPlan, operation, sequence) {
    return [
        'git-warp v18 scratch migration operation',
        '',
        `Migration-Format: git-warp-v18-scratch-operation-v1`,
        `Operation-Sequence: ${sequence}`,
        `Operation-Kind: ${operation.kind}`,
        `Source-Basis-UTF8-Hex: ${utf8Hex(patchPlan.sourceBasis.toKey())}`,
        `Target-Basis-UTF8-Hex: ${utf8Hex(patchPlan.targetBasis.toKey())}`,
        `Operation-Key-UTF8-Hex: ${utf8Hex(operation.toKey())}`,
        '',
    ].join('\n');
}
function utf8Hex(value) {
    const bytes = new TextEncoder().encode(value);
    const parts = [];
    for (const byte of bytes) {
        parts.push(byte.toString(16).padStart(2, '0'));
    }
    return parts.join('');
}
function requirePatchPlan(patchPlan) {
    if (!(patchPlan instanceof GraphModelMigrationLoweredPatchPlan)) {
        throw new GraphModelMigrationScratchWriterError('patchPlan must be a GraphModelMigrationLoweredPatchPlan');
    }
    return patchPlan;
}
function requireScratchRefName(scratchRefName) {
    if (scratchRefName === null) {
        throw new GraphModelMigrationScratchWriterError('scratchRefName must not be null after validation');
    }
    return scratchRefName;
}
function requireNonEmptyString(value, name) {
    if (typeof value !== 'string' || value.length === 0) {
        throw new GraphModelMigrationScratchWriterError(`${name} must be a non-empty string`);
    }
    return value;
}
async function gitText(cwd, args) {
    const result = await runMigrationGit(cwd, args, null);
    if (!result.ok()) {
        throw new GraphModelMigrationScratchWriterError(`git ${args.join(' ')} failed: ${result.stderr}`);
    }
    return result.stdout.trim();
}
async function gitTextWithInput(cwd, args, input, deterministicIdentity = false) {
    const result = await runMigrationGit(cwd, args, input, { deterministicIdentity });
    if (!result.ok()) {
        throw new GraphModelMigrationScratchWriterError(`git ${args.join(' ')} failed: ${result.stderr}`);
    }
    return result.stdout.trim();
}
async function gitTextOrNull(cwd, args) {
    const result = await runMigrationGit(cwd, args, null);
    if (!result.ok()) {
        return null;
    }
    return result.stdout.trim();
}
