/**
 * CasBlobAdapter — stores content blobs via git-cas.
 *
 * Content is chunked (CDC by default), optionally encrypted, and stored
 * as a CAS tree in the Git object store. The tree OID serves as the
 * storage identifier.
 *
 * Current runtime reads require CAS manifests. Migration tooling can inject an
 * explicit retired raw Git blob read policy while translating old substrates.
 *
 * @module infrastructure/adapters/CasBlobAdapter
 */
import BlobStoragePort from "../../ports/BlobStoragePort.js";
import PersistenceError from "../../domain/errors/PersistenceError.js";
import { createLazyCas } from "./lazyCasInit.js";
import { createCdcCasStore } from "./CasStoreFactory.js";
import { CURRENT_SUBSTRATE_ONLY_POLICY, } from "./SubstrateCompatibilityPolicy.js";
import CasContentEncryptionPolicy, { mapCasContentEncryptionError, } from "./CasContentEncryptionPolicy.js";
import { Readable } from 'node:stream';
function normalizeToUint8Array(buffer) {
    if (buffer instanceof Uint8Array && buffer.constructor !== Uint8Array) {
        return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
    }
    return buffer;
}
/**
 * Error codes from `@git-stunts/git-cas` that indicate the OID is not
 * a CAS manifest (i.e. it's a legacy raw Git blob written before the
 * CAS migration).
 *
 * - `MANIFEST_NOT_FOUND` — tree exists but contains no manifest entry
 * - `GIT_ERROR` — Git couldn't read the tree at all
 *
 * `GIT_ERROR` can mean either "wrong object type" or "missing object" depending
 * on the plumbing path. The legacy fallback path probes the raw object before
 * deciding whether this is retired legacy content or a genuinely missing OID.
 */
const LEGACY_BLOB_CODES = new Set(['MANIFEST_NOT_FOUND', 'GIT_ERROR']);
function isLegacyBlobError(err) {
    if (err instanceof Error && 'code' in err) {
        const coded = err;
        if (typeof coded.code === 'string') {
            return LEGACY_BLOB_CODES.has(coded.code);
        }
    }
    const msg = err instanceof Error ? err.message : '';
    return hasLegacyBlobMessage(msg);
}
function hasLegacyBlobMessage(msg) {
    return msg.includes('not a tree')
        || msg.includes('bad object')
        || msg.includes('does not exist');
}
function missingGitObjectError(oid, cause) {
    if (cause instanceof Error) {
        return new PersistenceError(`Missing Git object: ${oid}`, PersistenceError.E_MISSING_OBJECT, { cause, context: { oid } });
    }
    return new PersistenceError(`Missing Git object: ${oid}`, PersistenceError.E_MISSING_OBJECT, { context: { oid } });
}
export default class CasBlobAdapter extends BlobStoragePort {
    _plumbing;
    _persistence;
    _contentEncryption;
    _logger;
    _getCas;
    _compatibilityPolicy;
    constructor({ plumbing, persistence, encryptionKey, contentEncryption, logger, compatibilityPolicy }) {
        super();
        this._plumbing = plumbing;
        this._persistence = persistence;
        this._contentEncryption = resolveContentEncryption(contentEncryption, encryptionKey);
        this._logger = logger;
        this._getCas = createLazyCas(() => this._initCas());
        this._compatibilityPolicy = compatibilityPolicy ?? CURRENT_SUBSTRATE_ONLY_POLICY;
    }
    async _initCas() {
        return await createCdcCasStore({
            plumbing: this._plumbing,
            logger: this._logger,
        });
    }
    async store(content, options) {
        const cas = await this._getCas();
        const buf = typeof content === 'string'
            ? new TextEncoder().encode(content)
            : content;
        const source = Readable.from([buf]);
        const storeOpts = {
            source,
            slug: options?.slug ?? `blob-${Date.now().toString(36)}`,
            filename: 'content',
            ...this._contentEncryption.toStoreOptions(),
        };
        const manifest = await cas.store(storeOpts);
        return await cas.createTree({ manifest });
    }
    async retrieve(oid) {
        const cas = await this._getCas();
        try {
            return await this._restoreFromCas(cas, oid);
        }
        catch (err) {
            const encryptionError = mapCasContentEncryptionError(err, 'content-attachment');
            if (encryptionError !== null) {
                throw encryptionError;
            }
            if (!isLegacyBlobError(err)) {
                throw err;
            }
            return await this._readLegacyContentBlobCandidate(oid, err);
        }
    }
    async _restoreFromCas(cas, oid) {
        const manifest = await cas.readManifest({ treeOid: oid });
        const restoreOpts = this._buildRestoreOpts(manifest);
        const { buffer } = await cas.restore(restoreOpts);
        return normalizeToUint8Array(buffer);
    }
    async _fallbackReadBlob(oid) {
        const blob = await this._persistence.readBlob(oid);
        if (blob === null || blob === undefined) {
            throw new PersistenceError(`Missing Git object: ${oid}`, PersistenceError.E_MISSING_OBJECT, { context: { oid } });
        }
        return blob;
    }
    _buildRestoreOpts(manifest) {
        return { manifest, ...this._contentEncryption.toRestoreOptions() };
    }
    async storeStream(source, options) {
        const cas = await this._getCas();
        const readable = Readable.from(source);
        const storeOpts = {
            source: readable,
            slug: options?.slug ?? `blob-${Date.now().toString(36)}`,
            filename: 'content',
            ...this._contentEncryption.toStoreOptions(),
        };
        const manifest = await cas.store(storeOpts);
        return await cas.createTree({ manifest });
    }
    retrieveStream(oid) {
        const self = this;
        return {
            [Symbol.asyncIterator]() {
                let inner = null;
                let initialized = false;
                return {
                    async next() {
                        if (!initialized) {
                            initialized = true;
                            inner = await self._resolveStreamIterator(oid);
                        }
                        if (inner === null) {
                            return doneIteratorResult();
                        }
                        return await inner.next();
                    },
                    async return() {
                        if (inner !== null && typeof inner.return === 'function') {
                            return await inner.return();
                        }
                        return doneIteratorResult();
                    },
                };
            },
        };
    }
    async _resolveStreamIterator(oid) {
        const cas = await this._getCas();
        try {
            return await this._streamFromCas(cas, oid);
        }
        catch (err) {
            const encryptionError = mapCasContentEncryptionError(err, 'content-attachment-stream');
            if (encryptionError !== null) {
                throw encryptionError;
            }
            if (!isLegacyBlobError(err)) {
                throw err;
            }
            const blob = await this._readLegacyContentBlobCandidate(oid, err);
            return singleChunkIterator(blob);
        }
    }
    async _streamFromCas(cas, oid) {
        const manifest = await cas.readManifest({ treeOid: oid });
        const restoreOpts = this._buildRestoreOpts(manifest);
        if (typeof cas.restoreStream === 'function') {
            const stream = cas.restoreStream(restoreOpts);
            return stream[Symbol.asyncIterator]();
        }
        const { buffer } = await cas.restore(restoreOpts);
        return singleChunkIterator(buffer);
    }
    async _readLegacyContentBlobCandidate(oid, error) {
        if (this._compatibilityPolicy.legacyContentBlobReads) {
            return await this._fallbackReadBlob(oid);
        }
        const blob = await this._probeLegacyContentBlob(oid);
        if (blob === null) {
            throw missingGitObjectError(oid, error);
        }
        throw new PersistenceError(`Legacy raw blob reads require the substrate migration compatibility policy: ${oid}`, 'E_LEGACY_SUBSTRATE_DISABLED', {
            ...(error instanceof Error ? { cause: error } : {}),
            context: { oid },
        });
    }
    async _probeLegacyContentBlob(oid) {
        try {
            const blob = await this._persistence.readBlob(oid);
            return blob ?? null;
        }
        catch (err) {
            if (err instanceof PersistenceError && err.code === PersistenceError.E_MISSING_OBJECT) {
                return null;
            }
            throw err;
        }
    }
}
function resolveContentEncryption(contentEncryption, encryptionKey) {
    if (contentEncryption !== undefined) {
        return contentEncryption;
    }
    if (encryptionKey !== undefined) {
        return CasContentEncryptionPolicy.fromInternalResolvedKey({ encryptionKey });
    }
    return CasContentEncryptionPolicy.disabled();
}
function singleChunkIterator(buf) {
    let done = false;
    return {
        next() {
            if (done) {
                return Promise.resolve(doneIteratorResult());
            }
            done = true;
            return Promise.resolve({ value: buf, done: false });
        },
        return() {
            done = true;
            return Promise.resolve(doneIteratorResult());
        },
    };
}
function doneIteratorResult() {
    return { value: new Uint8Array(0), done: true };
}
