import TrieStoreError from "../../domain/errors/TrieStoreError.js";
import { getExitCode, gitDiagnosticText, toGitError, } from "./gitErrorClassification.js";
import { parseNibbleName } from "./trieNibbleName.js";
// -- Error codes -------------------------------------------------------------
const E_TRIE_STORE_READ = 'E_TRIE_STORE_READ';
const E_TRIE_STORE_WRITE = 'E_TRIE_STORE_WRITE';
const E_TRIE_STORE_MISSING = 'E_TRIE_STORE_MISSING';
const E_TRIE_STORE_CORRUPT = 'E_TRIE_STORE_CORRUPT';
// -- Git mode / type constants (native Git object encoding) ------------------
const BLOB_MODE = '100644';
const TREE_MODE = '040000';
const BLOB_TYPE = 'blob';
const TREE_TYPE = 'tree';
// -- Missing-object detection (for disambiguating read errors) ---------------
const MISSING_OBJECT_HINTS = [
    'bad object',
    'bad file',
    'not a valid object name',
    'does not point to a valid object',
    'missing object',
    'could not read',
    'not a tree object',
];
// -- Adapter ----------------------------------------------------------------
export default class GitTrieStoreAdapter {
    plumbing;
    constructor(deps) {
        if (deps === null || deps === undefined) {
            throw new TrieStoreError('plumbing dependency is required', {
                code: E_TRIE_STORE_WRITE,
                context: {},
            });
        }
        this.plumbing = deps.plumbing;
    }
    async readLeaf(oid) {
        return await this._readLeafBytes(oid);
    }
    async writeLeaf(data) {
        const input = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
        const out = await this._execWrite({
            args: ['hash-object', '-w', '-t', BLOB_TYPE, '--stdin'],
            input,
        });
        return out.trim();
    }
    async readBranch(oid) {
        const raw = await this._execReadString({
            args: ['ls-tree', '-z', oid],
            oid,
        });
        return parseBranchTreeOutput(raw);
    }
    async writeBranch(children) {
        const width = nibbleNameWidth(children);
        const lines = [];
        for (const [nibble, childOid] of children) {
            const kind = await this._probeObjectKind(childOid);
            lines.push(mktreeLine({ nibble, width, childOid, kind }));
        }
        const input = lines.length === 0 ? '' : `${lines.join('\n')}\n`;
        const out = await this._execWrite({ args: ['mktree'], input });
        return out.trim();
    }
    // -- Internal helpers ------------------------------------------------------
    async _readLeafBytes(oid) {
        try {
            const stream = await this.plumbing.executeStream({
                args: ['cat-file', BLOB_TYPE, oid],
            });
            const collected = await stream.collect({
                asString: false,
                maxBytes: Number.POSITIVE_INFINITY,
            });
            const bytes = bufferToUint8Array(collected);
            if (bytes.length === 0) {
                await this._assertObjectExists(oid);
            }
            return bytes;
        }
        catch (raw) {
            throw classifyReadFailure(raw, { oid });
        }
    }
    async _assertObjectExists(oid) {
        try {
            await this.plumbing.execute({ args: ['cat-file', '-e', oid] });
        }
        catch (raw) {
            // `cat-file -e` exits 1 with no stderr for a missing object —
            // that silent exit carries no diagnostic text the pattern
            // classifier can match, so short-circuit here.
            if (isSilentMissing(raw)) {
                throw new TrieStoreError(`Git object ${oid} does not exist`, {
                    code: E_TRIE_STORE_MISSING,
                    context: { oid },
                });
            }
            throw classifyReadFailure(raw, { oid });
        }
    }
    async _execReadString(opts) {
        try {
            return await this.plumbing.execute({ args: opts.args });
        }
        catch (raw) {
            throw classifyReadFailure(raw, { oid: opts.oid });
        }
    }
    async _execWrite(opts) {
        try {
            return await this.plumbing.execute({ args: opts.args, input: opts.input });
        }
        catch (raw) {
            throw classifyWriteFailure(raw);
        }
    }
    async _probeObjectKind(childOid) {
        let out;
        try {
            out = await this.plumbing.execute({ args: ['cat-file', '-t', childOid] });
        }
        catch (raw) {
            throw classifyReadFailure(raw, { oid: childOid });
        }
        const kind = out.trim();
        if (kind === BLOB_TYPE || kind === TREE_TYPE) {
            return kind;
        }
        throw new TrieStoreError(`child object ${childOid} has unsupported type "${kind}"`, {
            code: E_TRIE_STORE_WRITE,
            context: { oid: childOid, type: kind },
        });
    }
}
function mktreeLine(input) {
    const name = input.nibble.toString(16).padStart(input.width, '0');
    const mode = input.kind === BLOB_TYPE ? BLOB_MODE : TREE_MODE;
    return `${mode} ${input.kind} ${input.childOid}\t${name}`;
}
function nibbleNameWidth(children) {
    let maxNibble = 0;
    for (const nibble of children.keys()) {
        if (nibble > maxNibble) {
            maxNibble = nibble;
        }
    }
    const hexDigits = maxNibble === 0 ? 1 : Math.ceil(Math.log2(maxNibble + 1) / 4);
    return Math.max(1, hexDigits);
}
// -- Branch tree decoding ---------------------------------------------------
function parseBranchTreeOutput(raw) {
    const entries = new Map();
    if (raw.length === 0) {
        return entries;
    }
    for (const record of raw.split('\0')) {
        if (record === '') {
            continue;
        }
        const { nibble, childOid } = parseBranchTreeRecord(record);
        entries.set(nibble, childOid);
    }
    return entries;
}
function parseBranchTreeRecord(record) {
    const tabIndex = record.indexOf('\t');
    if (tabIndex === -1) {
        throw new TrieStoreError(`malformed ls-tree record: ${record}`, { code: E_TRIE_STORE_CORRUPT, context: { record } });
    }
    const meta = record.slice(0, tabIndex);
    const name = record.slice(tabIndex + 1);
    const parts = meta.split(' ');
    const childOid = parts[2];
    if (typeof childOid !== 'string' || childOid.length === 0) {
        throw new TrieStoreError(`ls-tree record missing OID: ${record}`, { code: E_TRIE_STORE_CORRUPT, context: { record } });
    }
    const nibble = parseNibbleName(name);
    return { nibble, childOid };
}
// -- Byte boundary helpers --------------------------------------------------
function bufferToUint8Array(collected) {
    if (typeof collected === 'string') {
        return new TextEncoder().encode(collected);
    }
    return new Uint8Array(collected.buffer.slice(collected.byteOffset, collected.byteOffset + collected.byteLength));
}
// -- Error classification ---------------------------------------------------
function classifyReadFailure(raw, hint) {
    const err = toGitError(raw);
    if (err instanceof TrieStoreError) {
        return err;
    }
    if (isMissingObject(err)) {
        return new TrieStoreError(`Git object ${hint.oid} does not exist`, {
            code: E_TRIE_STORE_MISSING,
            context: { oid: hint.oid, cause: err.message },
        });
    }
    return new TrieStoreError(`read failed for ${hint.oid}: ${err.message}`, {
        code: E_TRIE_STORE_READ,
        context: { oid: hint.oid, cause: err.message },
    });
}
function classifyWriteFailure(raw) {
    const err = toGitError(raw);
    if (err instanceof TrieStoreError) {
        return err;
    }
    return new TrieStoreError(`Git write failed: ${err.message}`, {
        code: E_TRIE_STORE_WRITE,
        context: { cause: err.message },
    });
}
function isMissingObject(err) {
    const code = getExitCode(err);
    if (code !== 128 && code !== 1) {
        return false;
    }
    const diag = gitDiagnosticText(err);
    const msg = err.message.toLowerCase();
    const haystack = `${diag} ${msg}`;
    return MISSING_OBJECT_HINTS.some((hint) => haystack.includes(hint));
}
function isSilentMissing(raw) {
    const err = toGitError(raw);
    if (err instanceof TrieStoreError) {
        return false;
    }
    if (getExitCode(err) !== 1) {
        return false;
    }
    return gitDiagnosticText(err) === '';
}
