import QueryError from "../../errors/QueryError.js";
import WarpMemoryPool from "../../memory/WarpMemoryPool.js";
import defaultCodec from "../../utils/defaultCodec.js";
import { CheckpointBasisFact } from "./CheckpointBasisFact.js";
import CheckpointBasisManifest, { CheckpointBasisChunking, CheckpointBasisCompleteness, CheckpointBasisShardGeometry, CheckpointBasisShardRootMap, CheckpointBasisSupportPosture, } from "./CheckpointBasisManifest.js";
const CHECKPOINT_BASIS_PENDING_FACT_SCOPE = 'checkpoint-basis-pending-fact';
export class StreamingCheckpointBasisBuildResult {
    manifest;
    rootTreeOid;
    flushCount;
    shardWriteCount;
    treeEntries;
    constructor(options) {
        this.manifest = options.manifest;
        this.rootTreeOid = options.rootTreeOid;
        this.flushCount = options.flushCount;
        this.shardWriteCount = options.shardWriteCount;
        this.treeEntries = Object.freeze([...options.treeEntries]);
        Object.freeze(this);
    }
}
export default class StreamingCheckpointBasisBuilder {
    _graphName;
    _checkpointSha;
    _frontier;
    _storage;
    _codec;
    _pool;
    _maxFactsPerShard;
    _layoutFamily;
    _payloadLayout;
    _shardKeyStrategy;
    constructor(options) {
        const validOptions = requireBuilderOptions(options);
        validateText(validOptions.graphName, 'graphName');
        validateText(validOptions.checkpointSha, 'checkpointSha');
        validateFrontier(validOptions.frontier);
        validateStorage(validOptions.storage);
        this._graphName = validOptions.graphName;
        this._checkpointSha = validOptions.checkpointSha;
        this._frontier = copyFrontier(validOptions.frontier);
        this._storage = validOptions.storage;
        this._codec = validOptions.codec ?? defaultCodec;
        this._pool = poolOrNull(validOptions.pool);
        this._maxFactsPerShard = validatePositiveInteger(validOptions.maxFactsPerShard, 'maxFactsPerShard');
        this._layoutFamily = validOptions.layoutFamily ?? 'checkpoint-basis-shards';
        this._payloadLayout = validOptions.payloadLayout ?? 'basis-facts-v1';
        this._shardKeyStrategy = validOptions.shardKeyStrategy ?? 'hex-prefix-2';
        Object.freeze(this);
    }
    async build(facts) {
        validateFactStream(facts);
        const publication = createCheckpointBasisPublication(this._maxFactsPerShard);
        const runtime = publicationRuntime(this._storage, this._codec, this._pool);
        try {
            for await (const fact of facts) {
                await publication.addFact(fact, runtime);
            }
            await publication.flushAll(runtime);
            const treeEntries = publication.treeEntries();
            const rootTreeOid = await this._storage.writeTree([...treeEntries]);
            return new StreamingCheckpointBasisBuildResult({
                manifest: this._manifest(publication, rootTreeOid),
                rootTreeOid,
                flushCount: publication.flushCount,
                shardWriteCount: publication.shardWriteCount,
                treeEntries,
            });
        }
        finally {
            publication.releaseAllLeases();
        }
    }
    _manifest(publication, rootTreeOid) {
        const shardCount = Math.max(1, publication.shardWriteCount);
        const chunkCount = Math.max(1, publication.flushCount);
        return new CheckpointBasisManifest({
            graphName: this._graphName,
            checkpointSha: this._checkpointSha,
            frontier: this._frontier,
            appliedVersionVector: appliedVersionVectorFromFrontier(this._frontier),
            basisIdentity: `basis:${this._graphName}:${this._checkpointSha}:${rootTreeOid}`,
            semanticReadingIdentity: `reading-basis:${this._graphName}:${this._checkpointSha}:streamed-facts-v1`,
            livenessRoots: publication.rootMap('node-liveness'),
            propertyRoots: publication.rootMap('node-property'),
            outgoingAdjacencyRoots: publication.rootMap('outgoing-adjacency'),
            incomingAdjacencyRoots: publication.rootMap('incoming-adjacency'),
            edgeFactRoots: publication.rootMap('edge-fact'),
            ...publicationPostures(publication, rootTreeOid),
            shardGeometry: this._shardGeometry(shardCount),
            chunking: new CheckpointBasisChunking({ maxFactsPerShard: this._maxFactsPerShard, chunkCount }),
            completeness: publication.shardWriteCount > 0
                ? CheckpointBasisCompleteness.complete()
                : CheckpointBasisCompleteness.partial('empty-fact-stream'),
        });
    }
    _shardGeometry(shardCount) {
        return new CheckpointBasisShardGeometry({
            layoutFamily: this._layoutFamily,
            payloadLayout: this._payloadLayout,
            shardKeyStrategy: this._shardKeyStrategy,
            shardCount,
        });
    }
}
class CheckpointBasisPendingShard {
    family;
    basePath;
    pendingKey;
    _facts;
    _leases;
    constructor(options) {
        this.family = options.family;
        this.basePath = options.basePath;
        this.pendingKey = options.pendingKey;
        this._facts = [];
        this._leases = [];
    }
    get size() {
        return this._facts.length;
    }
    add(fact, pool) {
        const lease = pool?.acquire({ scope: CHECKPOINT_BASIS_PENDING_FACT_SCOPE, amount: 1 }) ?? null;
        this._facts.push(fact);
        if (lease !== null) {
            this._leases.push(lease);
        }
    }
    takeSortedFacts() {
        const facts = [...this._facts].sort((left, right) => compareText(left.sortKey(), right.sortKey()));
        this._facts.length = 0;
        return Object.freeze(facts);
    }
    releaseLeases() {
        for (const lease of this._leases) {
            lease.release();
        }
        this._leases.length = 0;
    }
}
class CheckpointBasisPublication {
    _maxFactsPerShard;
    _pending;
    _chunkIndexes;
    _treeEntries;
    _livenessRoots;
    _propertyRoots;
    _outgoingAdjacencyRoots;
    _incomingAdjacencyRoots;
    _edgeFactRoots;
    _provenanceRoots;
    _contentAnchorRoots;
    flushCount;
    shardWriteCount;
    constructor(options) {
        this._maxFactsPerShard = options.maxFactsPerShard;
        this._pending = new Map();
        this._chunkIndexes = new Map();
        this._treeEntries = [];
        this._livenessRoots = new Map();
        this._propertyRoots = new Map();
        this._outgoingAdjacencyRoots = new Map();
        this._incomingAdjacencyRoots = new Map();
        this._edgeFactRoots = new Map();
        this._provenanceRoots = new Map();
        this._contentAnchorRoots = new Map();
        this.flushCount = 0;
        this.shardWriteCount = 0;
    }
    async addFact(fact, runtime) {
        validateFact(fact);
        const pendingShard = this._pendingShard(fact);
        pendingShard.add(fact, runtime.pool);
        if (pendingShard.size >= this._maxFactsPerShard) {
            await this._flush(pendingShard, runtime);
        }
    }
    async flushAll(runtime) {
        for (const pendingShard of this._pending.values()) {
            if (pendingShard.size > 0) {
                await this._flush(pendingShard, runtime);
            }
        }
    }
    releaseAllLeases() {
        for (const pendingShard of this._pending.values()) {
            pendingShard.releaseLeases();
        }
    }
    rootMap(family) {
        return new CheckpointBasisShardRootMap({
            family: manifestRootFamily(family),
            roots: this._rootsForFamily(family),
        });
    }
    hasFamily(family) {
        return this._rootsForFamily(family).size > 0;
    }
    treeEntries() {
        return Object.freeze([...this._treeEntries].sort(compareTreeEntries));
    }
    _pendingShard(fact) {
        const family = fact.shardFamily();
        const basePath = fact.shardPath();
        const pendingKey = `${family}/${basePath}`;
        const existing = this._pending.get(pendingKey);
        if (existing !== undefined) {
            return existing;
        }
        const created = new CheckpointBasisPendingShard({ family, basePath, pendingKey });
        this._pending.set(pendingKey, created);
        return created;
    }
    async _flush(pendingShard, runtime) {
        const facts = pendingShard.takeSortedFacts();
        if (facts.length === 0) {
            return;
        }
        const nextChunkIndex = this._nextChunkIndex(pendingShard.pendingKey);
        const chunkPath = chunkedPath(pendingShard.family, pendingShard.basePath, nextChunkIndex);
        try {
            const oid = await runtime.storage.writeBlob(runtime.codec.encode(facts.map((fact) => fact.toTransport())));
            this._rootsForFamily(pendingShard.family).set(chunkPath, oid);
            this._treeEntries.push(`100644 blob ${oid}\t${chunkPath}`);
            this.flushCount += 1;
            this.shardWriteCount += 1;
        }
        finally {
            pendingShard.releaseLeases();
        }
    }
    _nextChunkIndex(pendingKey) {
        const current = this._chunkIndexes.get(pendingKey) ?? 0;
        const next = current + 1;
        this._chunkIndexes.set(pendingKey, next);
        return next;
    }
    _rootsForFamily(family) {
        const store = this._rootStores().find((candidate) => candidate.family === family);
        if (store === undefined) {
            throwBuilderError('family', 'unsupported-root-family');
        }
        return store.roots;
    }
    _rootStores() {
        return Object.freeze([
            { family: 'node-liveness', roots: this._livenessRoots },
            { family: 'node-property', roots: this._propertyRoots },
            { family: 'outgoing-adjacency', roots: this._outgoingAdjacencyRoots },
            { family: 'incoming-adjacency', roots: this._incomingAdjacencyRoots },
            { family: 'edge-fact', roots: this._edgeFactRoots },
            { family: 'provenance', roots: this._provenanceRoots },
            { family: 'content-anchor', roots: this._contentAnchorRoots },
        ]);
    }
}
function createCheckpointBasisPublication(maxFactsPerShard) { return new CheckpointBasisPublication({ maxFactsPerShard }); }
function publicationRuntime(storage, codec, pool) {
    return Object.freeze({ storage, codec, pool });
}
function poolOrNull(pool) {
    if (pool === undefined || pool instanceof WarpMemoryPool) {
        return pool ?? null;
    }
    return throwBuilderError('pool', 'invalid-pool');
}
function requireBuilderOptions(options) {
    if (options === null || typeof options !== 'object') {
        throwBuilderError('options', 'invalid-options');
    }
    return options;
}
function publicationPostures(publication, rootTreeOid) {
    return {
        provenancePosture: publication.hasFamily('provenance')
            ? CheckpointBasisSupportPosture.present(`tree:${rootTreeOid}:provenance`)
            : CheckpointBasisSupportPosture.unavailable('no-provenance-facts'),
        contentAnchorPosture: publication.hasFamily('content-anchor')
            ? CheckpointBasisSupportPosture.present(`tree:${rootTreeOid}:content-anchor`)
            : CheckpointBasisSupportPosture.unavailable('no-content-anchor-facts'),
    };
}
function chunkedPath(family, basePath, chunkIndex) {
    return `${family}/${basePath}.chunk-${String(chunkIndex).padStart(6, '0')}`;
}
const MANIFEST_ROOT_FAMILIES = Object.freeze([
    'node-liveness',
    'node-property',
    'outgoing-adjacency',
    'incoming-adjacency',
    'edge-fact',
]);
function manifestRootFamily(family) {
    if (isManifestRootFamily(family)) {
        return family;
    }
    return throwBuilderError('family', 'not-a-manifest-root-family');
}
function isManifestRootFamily(family) {
    return MANIFEST_ROOT_FAMILIES.includes(family);
}
function appliedVersionVectorFromFrontier(frontier) {
    const versionVector = new Map();
    for (const writerId of [...frontier.keys()].sort()) {
        versionVector.set(writerId, 0);
    }
    return versionVector;
}
function copyFrontier(frontier) {
    return new Map([...frontier.entries()].sort(([left], [right]) => compareText(left, right)));
}
function compareTreeEntries(left, right) {
    const leftPath = left.slice(left.indexOf('\t') + 1);
    const rightPath = right.slice(right.indexOf('\t') + 1);
    return compareText(leftPath, rightPath);
}
function compareText(left, right) {
    return left < right ? -1 : left > right ? 1 : 0;
}
function validateFactStream(facts) {
    if (facts === null || facts === undefined || typeof facts[Symbol.asyncIterator] !== 'function') {
        throwBuilderError('facts', 'invalid-async-fact-stream');
    }
}
function validateFact(fact) {
    if (!(fact instanceof CheckpointBasisFact)) {
        throwBuilderError('fact', 'invalid-fact');
    }
}
function validateStorage(storage) {
    if (storage === null
        || storage === undefined
        || typeof storage.writeBlob !== 'function'
        || typeof storage.writeTree !== 'function') {
        throwBuilderError('storage', 'invalid-storage');
    }
}
function validateFrontier(frontier) {
    if (!(frontier instanceof Map) || frontier.size === 0) {
        throwBuilderError('frontier', 'invalid-frontier');
    }
    for (const [writerId, patchSha] of frontier) {
        validateText(writerId, 'frontier.writerId');
        validateText(patchSha, 'frontier.patchSha');
    }
}
function validateText(value, field) {
    if (typeof value !== 'string' || value.length === 0) {
        throwBuilderError(field, 'empty-string');
    }
    return value;
}
function validatePositiveInteger(value, field) {
    if (!Number.isInteger(value) || value <= 0) {
        throwBuilderError(field, 'invalid-positive-integer');
    }
    return value;
}
function throwBuilderError(field, reason) {
    throw new QueryError('Streaming checkpoint basis builder input is invalid.', {
        code: 'E_STREAMING_CHECKPOINT_BASIS_BUILDER',
        context: { field, reason },
    });
}
