/**
 * Git-backed trust chain adapter.
 *
 * Uses git-cas for CBOR blob storage (chunked, content-addressed,
 * streaming) and @git-stunts/plumbing for commit chain traversal
 * and ref management.
 *
 * Handles all encoding/decoding at the boundary:
 * - On READ: CBOR decode, recordId hash verification, signaturePayload
 *   precomputation, TrustRecord.fromDecoded()
 * - On WRITE: CBOR encode via git-cas, commit + tree, ref CAS
 *
 * @module infrastructure/adapters/GitTrustChainAdapter
 */
import TrustChainPort from "../../ports/TrustChainPort.js";
import { TrustRecord } from "../../domain/trust/TrustRecord.js";
import { recordIdPayload, signaturePayload } from "../../domain/trust/canonical.js";
import { textEncode } from "../../domain/utils/bytes.js";
import { buildTrustRecordRef } from "../../domain/utils/RefLayout.js";
import TrustError from "../../domain/errors/TrustError.js";
import { createLazyCas } from "./lazyCasInit.js";
import { loadGitCasConstructors } from "./gitCasModule.js";
import LoggerObservabilityBridge from "./LoggerObservabilityBridge.js";
import { CURRENT_SUBSTRATE_ONLY_POLICY, } from "./SubstrateCompatibilityPolicy.js";
import { Readable } from 'node:stream';
// -- Constants ----------------------------------------------------------------
const MAX_TRANSIENT_CAS_ATTEMPTS = 3;
const RECORD_BLOB_NAME = 'record.cbor';
// -- Plumbing helpers ---------------------------------------------------------
async function resolveRef(plumbing, ref) {
    try {
        const sha = await plumbing.execute({ args: ['rev-parse', '--verify', ref] });
        return sha.trim();
    }
    catch {
        return null;
    }
}
async function readCommitInfo(plumbing, commitSha) {
    const raw = await plumbing.execute({ args: ['cat-file', '-p', commitSha] });
    let treeSha = '';
    const parents = [];
    for (const line of raw.split('\n')) {
        if (line.startsWith('tree ')) {
            treeSha = line.slice(5).trim();
        }
        else if (line.startsWith('parent ')) {
            parents.push(line.slice(7).trim());
        }
        else if (line === '') {
            break;
        }
    }
    return { treeSha, parents };
}
async function readTreeEntries(plumbing, treeSha) {
    const raw = await plumbing.execute({ args: ['ls-tree', treeSha] });
    const entries = new Map();
    for (const line of raw.trim().split('\n')) {
        if (line.length === 0) {
            continue;
        }
        const tabIdx = line.indexOf('\t');
        if (tabIdx < 0) {
            continue;
        }
        const name = line.slice(tabIdx + 1);
        const parts = line.slice(0, tabIdx).split(' ');
        const oid = parts[2] ?? '';
        entries.set(name, oid);
    }
    return entries;
}
async function createCommit(plumbing, treeSha, parentSha, message) {
    const args = ['commit-tree', treeSha, '-m', message];
    if (parentSha !== null) {
        args.push('-p', parentSha);
    }
    const sha = await plumbing.execute({ args });
    return sha.trim();
}
async function compareAndSwapRef(plumbing, ref, newSha, expectedSha) {
    const args = (expectedSha !== null && expectedSha.length > 0)
        ? ['update-ref', ref, newSha, expectedSha]
        : ['update-ref', ref, newSha];
    await plumbing.execute({ args });
}
// -- Hash helpers (boundary concern) ------------------------------------------
async function computeRecordIdHash(record, crypto) {
    return await crypto.hash('sha256', recordIdPayload(record));
}
function computeSignaturePayloadBytes(record) {
    return textEncode(signaturePayload(record));
}
async function loadCborCodec() {
    const { CborCodecCtor } = await loadGitCasConstructors();
    return new CborCodecCtor();
}
// -- Adapter ------------------------------------------------------------------
export default class GitTrustChainAdapter extends TrustChainPort {
    _plumbing;
    _crypto;
    _logger;
    _getCas;
    _compatibilityPolicy;
    _cbor = null;
    constructor(deps) {
        super();
        this._plumbing = deps.plumbing;
        this._crypto = deps.crypto;
        this._logger = deps.logger;
        this._getCas = createLazyCas(() => this._initCas());
        this._compatibilityPolicy = deps.compatibilityPolicy ?? CURRENT_SUBSTRATE_ONLY_POLICY;
    }
    async _initCas() {
        const { ContentAddressableStore, CborCodecCtor } = await loadGitCasConstructors();
        const opts = {
            plumbing: this._plumbing,
            codec: new CborCodecCtor(),
            chunking: { strategy: 'cdc' },
        };
        if (this._logger) {
            opts.observability = new LoggerObservabilityBridge(this._logger);
        }
        return new ContentAddressableStore(opts);
    }
    async _getCbor() {
        if (!this._cbor) {
            this._cbor = await loadCborCodec();
        }
        return this._cbor;
    }
    // -- Port implementation: readTip -------------------------------------------
    async readTip(graphName) {
        const ref = buildTrustRecordRef(graphName);
        const tipSha = await resolveRef(this._plumbing, ref);
        if (tipSha === null) {
            return null;
        }
        const recordId = await this._readRecordIdFromCommit(tipSha);
        return { tipSha, recordId };
    }
    async _readRecordIdFromCommit(commitSha) {
        const cas = await this._getCas();
        const info = await readCommitInfo(this._plumbing, commitSha);
        try {
            const manifest = await cas.readManifest({ treeOid: info.treeSha });
            const restored = await cas.restore({ manifest });
            const cbor = await this._getCbor();
            const decoded = cbor.decode(restored.buffer);
            return decoded['recordId'] ?? null;
        }
        catch {
            this._requireLegacyTrustRecordPolicy(commitSha);
            // Fallback: try reading as raw blob (pre-CAS migration)
            const entries = await readTreeEntries(this._plumbing, info.treeSha);
            const manifestOid = entries.get(RECORD_BLOB_NAME);
            if (manifestOid === undefined) {
                return null;
            }
            return await this._readRecordIdRawFallback(manifestOid);
        }
    }
    async _readRecordIdRawFallback(blobOid) {
        try {
            const raw = await this._plumbing.execute({ args: ['cat-file', 'blob', blobOid] });
            const cbor = await this._getCbor();
            const decoded = cbor.decode(Buffer.from(raw, 'binary'));
            return decoded['recordId'] ?? null;
        }
        catch {
            return null;
        }
    }
    // -- Port implementation: readRecords (streaming) ---------------------------
    async *readRecords(graphName, tip) {
        const ref = buildTrustRecordRef(graphName);
        let currentSha = tip ?? await resolveRef(this._plumbing, ref);
        if (currentSha === null) {
            return;
        }
        // Walk backward, collecting SHAs for forward iteration
        const commitShas = [];
        while (currentSha !== null) {
            commitShas.push(currentSha);
            const info = await readCommitInfo(this._plumbing, currentSha);
            currentSha = info.parents[0] ?? null;
        }
        // Yield records oldest-first
        for (let i = commitShas.length - 1; i >= 0; i--) {
            const sha = commitShas[i];
            const record = await this._decodeRecordFromCommit(sha);
            if (record) {
                yield record;
            }
        }
    }
    async _decodeRecordFromCommit(commitSha) {
        const info = await readCommitInfo(this._plumbing, commitSha);
        const cas = await this._getCas();
        const cbor = await this._getCbor();
        let rawRecord;
        try {
            const manifest = await cas.readManifest({ treeOid: info.treeSha });
            const restored = await cas.restore({ manifest });
            rawRecord = cbor.decode(restored.buffer);
        }
        catch {
            this._requireLegacyTrustRecordPolicy(commitSha);
            // Fallback: pre-CAS raw blob
            const entries = await readTreeEntries(this._plumbing, info.treeSha);
            const blobOid = entries.get(RECORD_BLOB_NAME);
            if (blobOid === undefined) {
                return null;
            }
            const raw = await this._plumbing.execute({ args: ['cat-file', 'blob', blobOid] });
            rawRecord = cbor.decode(Buffer.from(raw, 'binary'));
        }
        // Verify recordId at boundary
        const expectedId = await computeRecordIdHash(rawRecord, this._crypto);
        const actualId = rawRecord['recordId'];
        if (typeof actualId !== 'string' || actualId !== expectedId) {
            throw new TrustError(`RecordId mismatch: expected ${expectedId}, got ${JSON.stringify(actualId)}`, { code: 'E_TRUST_RECORD_ID_MISMATCH' });
        }
        // Precompute signature payload from raw canonical form
        const sigPayload = computeSignaturePayloadBytes(rawRecord);
        return TrustRecord.fromDecoded({
            schemaVersion: rawRecord['schemaVersion'],
            recordType: rawRecord['recordType'],
            recordId: rawRecord['recordId'],
            issuerKeyId: rawRecord['issuerKeyId'],
            issuedAt: rawRecord['issuedAt'],
            prev: rawRecord['prev'],
            subject: rawRecord['subject'],
            meta: (rawRecord['meta'] ?? {}),
            signature: rawRecord['signature'],
            signaturePayload: sigPayload,
        });
    }
    _requireLegacyTrustRecordPolicy(commitSha) {
        if (this._compatibilityPolicy.legacyTrustRecordBlobReads) {
            return;
        }
        throw new TrustError(`Legacy trust record blob reads require the substrate migration compatibility policy: ${commitSha}`, { code: 'E_LEGACY_SUBSTRATE_DISABLED', context: { commitSha } });
    }
    // -- Port implementation: persistRecord -------------------------------------
    async persistRecord(graphName, record, parentTipSha) {
        const ref = buildTrustRecordRef(graphName);
        const cas = await this._getCas();
        const cbor = await this._getCbor();
        // Encode record as CBOR → store via git-cas
        const recordObj = {
            schemaVersion: record.schemaVersion,
            recordType: record.recordType,
            recordId: record.recordId,
            issuerKeyId: record.issuerKeyId,
            issuedAt: record.issuedAt,
            prev: record.prev,
            subject: record.subject,
            meta: record.meta,
            signature: record.signature,
        };
        const encoded = cbor.encode(recordObj);
        const source = Readable.from([encoded]);
        const manifest = await cas.store({
            source,
            slug: `trust-${record.recordId.slice(0, 12)}`,
            filename: RECORD_BLOB_NAME,
        });
        const treeOid = await cas.createTree({ manifest });
        // Create commit
        const message = `trust: ${record.recordType} ${record.recordId.slice(0, 12)}`;
        const commitSha = await createCommit(this._plumbing, treeOid, parentTipSha, message);
        // CAS ref update with transient retry
        await this._casUpdateRef(ref, commitSha, parentTipSha, graphName);
        return commitSha;
    }
    async _casUpdateRef(ref, commitSha, expectedSha, _graphName) {
        for (let attempt = 1; attempt <= MAX_TRANSIENT_CAS_ATTEMPTS; attempt++) {
            try {
                await compareAndSwapRef(this._plumbing, ref, commitSha, expectedSha);
                return;
            }
            catch {
                // Distinguish transient vs real conflict
                const freshTipSha = await resolveRef(this._plumbing, ref);
                if (freshTipSha === expectedSha) {
                    // Transient failure — retry
                    if (attempt === MAX_TRANSIENT_CAS_ATTEMPTS) {
                        throw new TrustError(`Trust CAS exhausted after ${MAX_TRANSIENT_CAS_ATTEMPTS} attempts`, { code: 'E_TRUST_CAS_EXHAUSTED' });
                    }
                    continue;
                }
                // Real conflict — chain advanced
                const freshRecordId = freshTipSha !== null
                    ? await this._readRecordIdFromCommit(freshTipSha)
                    : null;
                throw new TrustError(`Trust CAS conflict: chain advanced from ${String(expectedSha)} to ${String(freshTipSha)}`, {
                    code: 'E_TRUST_CAS_CONFLICT',
                    context: {
                        expectedTipSha: expectedSha,
                        actualTipSha: freshTipSha,
                        actualTipRecordId: freshRecordId,
                    },
                });
            }
        }
    }
}
