/**
 * Git-backed persistence adapter for WARP graph storage.
 *
 * Translates graph operations into Git plumbing commands. Implements the
 * composite GraphPersistencePort (CommitPort + BlobPort + TreePort +
 * RefPort + ConfigPort).
 */
import { GitPersistenceAdapter } from '@git-stunts/git-cas';
import AdapterValidationError from "../../domain/errors/AdapterValidationError.js";
import PersistenceError from "../../domain/errors/PersistenceError.js";
import GraphPersistencePort from "../../ports/GraphPersistencePort.js";
import CasBlobAdapter from "./CasBlobAdapter.js";
import GitCasGraphReaderAdapter from "./GitCasGraphReaderAdapter.js";
import GitRecursiveTreeOidReaderAdapter from "./GitRecursiveTreeOidReaderAdapter.js";
import GitTrieStoreAdapter from "./GitTrieStoreAdapter.js";
import AlfredOperationPolicyAdapter from "./AlfredOperationPolicyAdapter.js";
import WarpStream from "../../domain/stream/WarpStream.js";
import { textEncode } from "../../domain/utils/bytes.js";
import { validateOid, validateRef, validateLimit, validateConfigKey } from "./adapterValidation.js";
import { getExitCode, isDanglingObjectError, gitDiagnosticText, wrapGitError, toGitError, DEFAULT_RETRY_OPTIONS, } from "./gitErrorClassification.js";
import { createGitCasPatchStorage } from "../../ports/CommitMessageCodecPort.js";
/**
 * Normalizes graph blob writes to the content shape expected by git-cas.
 */
function toGitCasBlobContent(content) {
    if (typeof content === 'string') {
        return textEncode(content);
    }
    return content;
}
/**
 * Adapts git-warp retry options to git-cas's policy-shaped boundary.
 */
function createGitCasRetryPolicy(policy, retryOptions) {
    return Object.freeze({
        async execute(operation) {
            return await policy.execute(operation, retryOptions);
        },
    });
}
function parseContentAnchorObjectType(value, oid) {
    if (value === 'blob' || value === 'tree') {
        return value;
    }
    throw new PersistenceError(`Unsupported Git object type for content anchor ${oid}: ${value}`, 'E_UNSUPPORTED_CONTENT_ANCHOR_OBJECT_TYPE', { context: { oid, objectType: value } });
}
export default class GitGraphAdapter extends GraphPersistencePort {
    plumbing;
    _policy;
    _retryOptions;
    _gitCasPersistence;
    _gitCasGraphReader;
    _recursiveTreeOidReader;
    _casContentEncryption;
    constructor({ plumbing, retryOptions = {}, policy, casContentEncryption }) {
        super();
        if (plumbing === null || plumbing === undefined) {
            throw new AdapterValidationError('plumbing is required');
        }
        this.plumbing = plumbing;
        this._casContentEncryption = casContentEncryption;
        this._retryOptions = { ...DEFAULT_RETRY_OPTIONS, ...retryOptions };
        this._policy = policy ?? new AlfredOperationPolicyAdapter({
            retryOptions: this._retryOptions,
        });
        this._gitCasPersistence = new GitPersistenceAdapter({
            plumbing,
            policy: createGitCasRetryPolicy(this._policy, this._retryOptions),
        });
        this._recursiveTreeOidReader = new GitRecursiveTreeOidReaderAdapter({
            plumbing,
            policy: this._policy,
            retryOptions: this._retryOptions,
        });
        this._gitCasGraphReader = new GitCasGraphReaderAdapter({
            persistence: this._gitCasPersistence,
            assertEmptyBlobExists: (oid) => this._assertBlobExistsForEmptyRead(oid),
            treeOidReader: this._recursiveTreeOidReader,
        });
    }
    async _executeWithRetry(options) {
        return await this._policy.execute(() => this.plumbing.execute(options), this._retryOptions);
    }
    /**
     * Distinguishes a legitimate zero-byte blob from a missing object when a
     * blob stream returns no bytes.
     */
    async _assertBlobExistsForEmptyRead(oid) {
        try {
            await this._executeWithRetry({ args: ['cat-file', '-e', oid] });
        }
        catch (raw) {
            const err = toGitError(raw);
            const wrapped = wrapGitError(err, { oid });
            const exitCode = getExitCode(err);
            const diagnostics = gitDiagnosticText(err);
            const ambiguousMissingObject = exitCode === 1 && diagnostics === '';
            if (wrapped === err && ambiguousMissingObject) {
                throw new PersistenceError(`Missing Git object: ${oid}`, PersistenceError.E_MISSING_OBJECT, { cause: err, context: { oid } });
            }
            throw wrapped;
        }
    }
    get emptyTree() {
        return this.plumbing.emptyTree;
    }
    createRuntimeBlobStorage() {
        return Promise.resolve(new CasBlobAdapter({
            plumbing: this.plumbing,
            persistence: this,
            ...(this._casContentEncryption ? { contentEncryption: this._casContentEncryption } : {}),
        }));
    }
    createRuntimeTrieStore() {
        return Promise.resolve(new GitTrieStoreAdapter({
            plumbing: this.plumbing,
        }));
    }
    defaultPatchWriteStorage() {
        return createGitCasPatchStorage(false);
    }
    async _createCommit(opts) {
        for (const p of opts.parents) {
            validateOid(p);
        }
        const parentArgs = opts.parents.flatMap(p => ['-p', p]);
        const signArgs = opts.sign ? ['-S'] : [];
        const args = ['commit-tree', opts.tree, ...parentArgs, ...signArgs, '-m', opts.message];
        const oid = await this._executeWithRetry({ args });
        return oid.trim();
    }
    async commitNode({ message, parents = [], sign = false }) {
        return await this._createCommit({ tree: this.emptyTree, parents, message, sign });
    }
    async commitNodeWithTree({ treeOid, parents = [], message, sign = false }) {
        validateOid(treeOid);
        return await this._createCommit({ tree: treeOid, parents, message, sign });
    }
    async showNode(sha) {
        validateOid(sha);
        try {
            return await this._executeWithRetry({ args: ['show', '-s', '--format=%B', sha] });
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { oid: sha });
        }
    }
    async getNodeInfo(sha) {
        validateOid(sha);
        const format = '%H%x00%an <%ae>%x00%aI%x00%P%x00%B';
        let output;
        try {
            output = await this._executeWithRetry({ args: ['show', '-s', `--format=${format}`, sha] });
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { oid: sha });
        }
        const parts = output.split('\x00');
        if (parts.length < 5) {
            throw new PersistenceError(`Invalid commit format for SHA ${sha}`, PersistenceError.E_MISSING_OBJECT, { context: { oid: sha } });
        }
        const [commitSha, author, date, parentsStr, ...messageParts] = parts;
        const message = messageParts.join('\x00');
        const parents = (parentsStr !== undefined && parentsStr.length > 0) ? parentsStr.split(' ').filter(p => p !== '') : [];
        return {
            sha: (commitSha ?? '').trim(),
            message,
            author: (author ?? '').trim(),
            date: (date ?? '').trim(),
            parents,
        };
    }
    async getCommitTree(sha) {
        validateOid(sha);
        try {
            const output = await this._executeWithRetry({ args: ['rev-parse', `${sha}^{tree}`] });
            return output.trim();
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { oid: sha });
        }
    }
    async logNodes({ ref, limit = 50, format }) {
        validateRef(ref);
        validateLimit(limit);
        const args = ['log', `-${limit}`];
        if (typeof format === 'string' && format.length > 0) {
            args.push(`--format=${format}`);
        }
        args.push(ref);
        try {
            return await this._executeWithRetry({ args });
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { ref });
        }
    }
    async logNodesStream({ ref, limit = 1000000, format }) {
        validateRef(ref);
        validateLimit(limit);
        const args = ['log', '-z', `-${limit}`];
        if (typeof format === 'string' && format.length > 0) {
            // Strip NUL bytes — Git -z uses NUL as record terminator.
            // eslint-disable-next-line no-control-regex
            const cleanFormat = format.replace(/\x00/g, '');
            args.push(`--format=${cleanFormat}`);
        }
        args.push(ref);
        const rawStream = await this._policy.stream(() => this.plumbing.executeStream({ args }), this._retryOptions);
        return WarpStream.from(rawStream);
    }
    async writeBlob(content) {
        const oid = await this._gitCasPersistence.writeBlob(toGitCasBlobContent(content));
        return oid.trim();
    }
    async writeTree(entries) {
        const oid = await this._gitCasPersistence.writeTree(entries);
        return oid.trim();
    }
    async readTree(treeOid) {
        const oids = await this.readTreeOids(treeOid);
        const files = new Map();
        const entries = Object.entries(oids);
        const batchSize = 16;
        for (let i = 0; i < entries.length; i += batchSize) {
            const batch = entries.slice(i, i + batchSize);
            const results = await Promise.all(batch.map(([, oid]) => this.readBlob(oid)));
            for (let j = 0; j < batch.length; j++) {
                const entry = batch[j];
                const result = results[j];
                if (entry !== undefined && result !== undefined) {
                    files.set(entry[0], result);
                }
            }
        }
        return Object.fromEntries(files);
    }
    async readTreeOids(treeOid) {
        validateOid(treeOid);
        try {
            return await this._gitCasGraphReader.readTreeOids(treeOid);
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { oid: treeOid });
        }
    }
    async readTreeEntryOid(treeOid, path) {
        return await this._recursiveTreeOidReader.readTreeEntryOid(treeOid, path);
    }
    async readTreeEntryPrefix(treeOid, prefix, limit) {
        return await this._recursiveTreeOidReader.readTreeEntryPrefix(treeOid, prefix, limit);
    }
    async readBlob(oid) {
        validateOid(oid);
        try {
            return await this._gitCasGraphReader.readBlob(oid);
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { oid });
        }
    }
    async readObjectType(oid) {
        validateOid(oid);
        let output;
        try {
            output = await this._executeWithRetry({ args: ['cat-file', '-t', oid] });
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { oid });
        }
        return parseContentAnchorObjectType(output.trim(), oid);
    }
    async updateRef(ref, oid) {
        validateRef(ref);
        validateOid(oid);
        try {
            await this._executeWithRetry({ args: ['update-ref', ref, oid] });
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { ref, oid });
        }
    }
    async readRef(ref) {
        validateRef(ref);
        try {
            const oid = await this._executeWithRetry({ args: ['rev-parse', '--verify', '--quiet', ref] });
            return oid.trim();
        }
        catch (raw) {
            const err = toGitError(raw);
            if (getExitCode(err) === 1) {
                return null;
            }
            if (isDanglingObjectError(err)) {
                return null;
            }
            throw wrapGitError(err, { ref });
        }
    }
    async compareAndSwapRef(ref, newOid, expectedOid) {
        validateRef(ref);
        validateOid(newOid);
        const oldArg = expectedOid ?? '0'.repeat(40);
        if (expectedOid !== null && expectedOid !== undefined) {
            validateOid(expectedOid);
        }
        // Direct call — CAS failures must NOT be retried.
        try {
            await this.plumbing.execute({ args: ['update-ref', ref, newOid, oldArg] });
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { ref, oid: newOid });
        }
    }
    async deleteRef(ref) {
        validateRef(ref);
        try {
            await this._executeWithRetry({ args: ['update-ref', '-d', ref] });
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { ref });
        }
    }
    async nodeExists(sha) {
        validateOid(sha);
        try {
            await this._executeWithRetry({ args: ['cat-file', '-e', sha] });
            return true;
        }
        catch (raw) {
            if (getExitCode(toGitError(raw)) === 1) {
                return false;
            }
            throw raw;
        }
    }
    async listRefs(prefix, options) {
        validateRef(prefix);
        const limit = options?.limit;
        const args = ['for-each-ref', '--format=%(refname)'];
        if (limit !== null && limit !== undefined && limit !== 0) {
            validateLimit(limit);
            args.push(`--count=${limit}`);
        }
        args.push(prefix);
        let output;
        try {
            output = await this._executeWithRetry({ args });
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { ref: prefix });
        }
        return output.split('\n').filter(line => line.trim() !== '');
    }
    async ping() {
        const start = Date.now();
        try {
            await this._executeWithRetry({ args: ['rev-parse', '--is-inside-work-tree'] });
            return { ok: true, latencyMs: Date.now() - start };
        }
        catch {
            return { ok: false, latencyMs: Date.now() - start };
        }
    }
    async countNodes(ref) {
        validateRef(ref);
        try {
            const output = await this._executeWithRetry({ args: ['rev-list', '--count', ref] });
            return parseInt(output.trim(), 10);
        }
        catch (raw) {
            throw wrapGitError(toGitError(raw), { ref });
        }
    }
    async isAncestor(potentialAncestor, descendant) {
        validateOid(potentialAncestor);
        validateOid(descendant);
        try {
            await this._executeWithRetry({
                args: ['merge-base', '--is-ancestor', potentialAncestor, descendant],
            });
            return true;
        }
        catch (raw) {
            if (getExitCode(toGitError(raw)) === 1) {
                return false;
            }
            throw raw;
        }
    }
    async configGet(key) {
        validateConfigKey(key);
        try {
            const value = await this._executeWithRetry({ args: ['config', '--get', key] });
            return value.replace(/\n$/, '');
        }
        catch (raw) {
            if (this._isConfigKeyNotFound(toGitError(raw))) {
                return null;
            }
            throw raw;
        }
    }
    async configSet(key, value) {
        validateConfigKey(key);
        if (typeof value !== 'string') {
            throw new AdapterValidationError('Config value must be a string');
        }
        await this._executeWithRetry({ args: ['config', key, value] });
    }
    _isConfigKeyNotFound(err) {
        return getExitCode(err) === 1;
    }
}
