import WarpError from "../../errors/WarpError.js";
function coordinateKey(coordinate) {
    const parts = [...coordinate.frontier.entries()]
        .sort(([leftWriter], [rightWriter]) => leftWriter < rightWriter ? -1 : leftWriter > rightWriter ? 1 : 0)
        .map(([writerId, tipSha]) => `${writerId}:${tipSha}`);
    const ceiling = coordinate.ceiling === null ? 'null' : String(coordinate.ceiling);
    return `${ceiling}|${parts.join('|')}`;
}
function candidateRank(candidate) {
    return candidate.coordinate.ceiling ?? Number.MIN_SAFE_INTEGER;
}
function recencyTimestamp(candidate) {
    return candidate.lastAccessedAt ?? candidate.createdAt;
}
export default class WarpStateSnapshotIndex {
    _isCoordinateCompatible;
    _byId;
    _byCoordinateKey;
    constructor(options) {
        if (typeof options.isCoordinateCompatible !== 'function') {
            throw new WarpError('WarpStateSnapshotIndex requires a compatibility predicate', 'E_SNAPSHOT_INDEX_COMPATIBILITY');
        }
        this._isCoordinateCompatible = options.isCoordinateCompatible;
        this._byId = new Map();
        this._byCoordinateKey = new Map();
    }
    upsert(snapshot) {
        this._byId.set(snapshot.snapshotId, snapshot);
        this._byCoordinateKey.set(coordinateKey(snapshot.coordinate), snapshot.snapshotId);
    }
    findExact(coordinate) {
        const snapshotId = this._byCoordinateKey.get(coordinateKey(coordinate));
        if (snapshotId === undefined) {
            return null;
        }
        return this._byId.get(snapshotId) ?? null;
    }
    findBestCompatiblePredecessor(coordinate) {
        let best = null;
        for (const candidate of this._byId.values()) {
            if (!this._isCoordinateCompatible(candidate.coordinate, coordinate)) {
                continue;
            }
            if (best === null) {
                best = candidate;
                continue;
            }
            const bestRank = candidateRank(best);
            const candidateValue = candidateRank(candidate);
            if (candidateValue > bestRank) {
                best = candidate;
                continue;
            }
            if (candidateValue < bestRank) {
                continue;
            }
            if (recencyTimestamp(candidate) > recencyTimestamp(best)) {
                best = candidate;
            }
        }
        return best;
    }
    findById(snapshotId) {
        return this._byId.get(snapshotId) ?? null;
    }
    pruneEvictable(options) {
        const evictable = [...this._byId.values()]
            .filter((snapshot) => snapshot.retention === 'evictable')
            .sort((left, right) => {
            const leftTimestamp = recencyTimestamp(left);
            const rightTimestamp = recencyTimestamp(right);
            if (leftTimestamp < rightTimestamp) {
                return -1;
            }
            if (leftTimestamp > rightTimestamp) {
                return 1;
            }
            return left.snapshotId < right.snapshotId ? -1 : left.snapshotId > right.snapshotId ? 1 : 0;
        });
        if (evictable.length <= options.maxEntries) {
            return;
        }
        const toRemove = evictable.slice(0, evictable.length - options.maxEntries);
        for (const snapshot of toRemove) {
            this._byId.delete(snapshot.snapshotId);
            this._byCoordinateKey.delete(coordinateKey(snapshot.coordinate));
        }
    }
}
