import StrandError from "../../errors/StrandError.js";
import { STRAND_COUNTERFACTUAL_REASON, compareStrings, maxPatchLamport, } from "./strandShared.js";
function footprintToSet(footprint) {
    return new Set([...footprint.reads, ...footprint.writes]);
}
function setsOverlap(left, right) {
    for (const value of left) {
        if (right.has(value)) {
            return true;
        }
    }
    return false;
}
export default class StrandIntentService {
    _graph;
    _loadStrandOrThrow;
    _buildQueuedIntent;
    _writeDescriptor;
    _commitQueuedPatch;
    _collectPatchEntries;
    _buildTickId;
    _counterfactualReason;
    /**
     * Create an intent/tick boundary over strand queueing, classification, and drain persistence.
     */
    constructor({ graph, loadStrandOrThrow, buildQueuedIntent, writeDescriptor, commitQueuedPatch, collectPatchEntries, buildTickId, }) {
        this._graph = graph;
        this._loadStrandOrThrow = loadStrandOrThrow;
        this._buildQueuedIntent = buildQueuedIntent;
        this._writeDescriptor = writeDescriptor;
        this._commitQueuedPatch = commitQueuedPatch;
        this._collectPatchEntries = collectPatchEntries;
        this._buildTickId = buildTickId;
        this._counterfactualReason = STRAND_COUNTERFACTUAL_REASON;
    }
    /**
     * Enqueue a new intent onto the strand's intent queue for deferred tick processing.
     */
    async queueIntent(strandId, build) {
        this._assertNotReentrant('graph.queueStrandIntent() is not reentrant. Use queueStrandIntent() from one build callback at a time.');
        this._graph._patchInProgress = true;
        try {
            const descriptor = await this._loadStrandOrThrow(strandId);
            const queuedIntent = await this._buildQueuedIntent(descriptor, build);
            await this._storeQueuedIntent(descriptor, queuedIntent);
            return queuedIntent;
        }
        finally {
            this._graph._patchInProgress = false;
        }
    }
    /**
     * Return all queued intents for a strand as frozen snapshots.
     */
    async listIntents(strandId) {
        const descriptor = await this._loadStrandOrThrow(strandId);
        // descriptor.intentQueue is already typed by the hydration path;
        // no need to re-normalize.
        return this._freezeQueuedIntentSnapshots(descriptor.intentQueue.intents);
    }
    /**
     * Drain the intent queue, classify and commit admitted intents, and record the tick.
     */
    async tick(strandId) {
        const tickContext = await this._loadTickContext(strandId);
        const { admitted, rejected } = this.classifyQueuedIntents(tickContext.queuedIntents);
        const committed = await this.commitAdmittedQueuedIntents(tickContext.descriptor, admitted);
        const tickRecord = this._buildTickRecord({
            tickContext,
            admitted,
            rejected,
            committed,
        });
        await this.persistTickResult({
            descriptor: tickContext.descriptor,
            intentQueue: tickContext.intentQueue,
            tickIndex: tickContext.tickIndex,
            now: tickContext.now,
            committed,
            tickRecord,
        });
        return tickRecord;
    }
    /**
     * Partition queued intents into admitted and rejected sets based on footprint overlap.
     */
    classifyQueuedIntents(queuedIntents) {
        const admitted = [];
        const rejected = [];
        for (const intent of queuedIntents) {
            const footprint = footprintToSet(intent);
            const conflictsWith = this._findIntentConflicts(admitted, footprint);
            if (conflictsWith.length > 0) {
                rejected.push(this._buildRejectedCounterfactual(intent, conflictsWith));
            }
            else {
                admitted.push({ ...intent, footprint });
            }
        }
        return { admitted, rejected };
    }
    /**
     * Sequentially commit all admitted intents to the overlay patch chain.
     */
    async commitAdmittedQueuedIntents(descriptor, admitted) {
        let overlayHeadPatchSha = descriptor.overlay.headPatchSha ?? null;
        let overlayPatchCount = descriptor.overlay.patchCount;
        let maxLamport = await this._loadCurrentLamport(descriptor);
        const overlayPatchShas = [];
        for (const intent of admitted) {
            maxLamport += 1;
            const committed = await this._commitQueuedPatch({
                strandId: descriptor.strandId,
                overlayId: descriptor.overlay.overlayId,
                parentSha: overlayHeadPatchSha,
                patch: intent.patch,
                contentBlobOids: intent.contentBlobOids,
                lamport: maxLamport,
            });
            overlayHeadPatchSha = committed.sha;
            overlayPatchCount += 1;
            overlayPatchShas.push(committed.sha);
        }
        return {
            overlayHeadPatchSha,
            overlayPatchCount,
            overlayPatchShas,
            maxLamport,
        };
    }
    /**
     * Persist the tick result by updating the descriptor and graph-side cache flags.
     */
    async persistTickResult({ descriptor, intentQueue, tickIndex, now, committed, tickRecord, }) {
        await this._writeDescriptor(this._buildPersistedTickDescriptor({
            descriptor,
            intentQueue,
            tickIndex,
            now,
            committed,
            tickRecord,
        }));
        this._updateTickCaches(committed.maxLamport);
    }
    _assertNotReentrant(message) {
        if (this._graph._patchInProgress) {
            throw new StrandError(message, { code: 'E_STRAND_REENTRANT' });
        }
    }
    async _storeQueuedIntent(descriptor, queuedIntent) {
        const { intentQueue } = descriptor;
        const now = String(this._graph._maxObservedLamport);
        await this._writeDescriptor({
            ...descriptor,
            updatedAt: now,
            intentQueue: {
                nextIntentSeq: intentQueue.nextIntentSeq + 1,
                intents: [...intentQueue.intents, queuedIntent].sort((left, right) => compareStrings(left.intentId, right.intentId)),
            },
        });
        this._graph._cachedViewHash = null;
    }
    _freezeQueuedIntentSnapshots(intents) {
        return intents.map((intent) => Object.freeze({
            ...intent,
            reads: [...intent.reads],
            writes: [...intent.writes],
            contentBlobOids: [...intent.contentBlobOids],
        }));
    }
    async _loadTickContext(strandId) {
        const descriptor = await this._loadStrandOrThrow(strandId);
        const { intentQueue, evolution } = descriptor;
        return {
            descriptor,
            intentQueue,
            queuedIntents: [...intentQueue.intents].sort((left, right) => compareStrings(left.intentId, right.intentId)),
            tickIndex: evolution.tickCount + 1,
            now: String(this._graph._maxObservedLamport),
        };
    }
    _buildRejectedCounterfactual(intent, conflictsWith) {
        return {
            intentId: intent.intentId,
            reason: this._counterfactualReason,
            conflictsWith,
            reads: [...intent.reads],
            writes: [...intent.writes],
        };
    }
    _findIntentConflicts(admitted, footprint) {
        return admitted
            .filter((candidate) => setsOverlap(candidate.footprint, footprint))
            .map((candidate) => candidate.intentId);
    }
    async _loadCurrentLamport(descriptor) {
        return maxPatchLamport(await this._collectPatchEntries(descriptor, { ceiling: null }));
    }
    _buildTickRecord({ tickContext, admitted, rejected, committed, }) {
        return Object.freeze({
            tickId: this._buildTickId(tickContext.descriptor.strandId, tickContext.tickIndex),
            strandId: tickContext.descriptor.strandId,
            tickIndex: tickContext.tickIndex,
            createdAt: tickContext.now,
            drainedIntentCount: tickContext.queuedIntents.length,
            admittedIntentIds: admitted.map((intent) => intent.intentId),
            rejected,
            baseOverlayHeadPatchSha: tickContext.descriptor.overlay.headPatchSha ?? null,
            overlayHeadPatchSha: committed.overlayHeadPatchSha,
            overlayPatchShas: committed.overlayPatchShas,
        });
    }
    _buildPersistedTickDescriptor({ descriptor, intentQueue, tickIndex, now, committed, tickRecord, }) {
        return {
            ...descriptor,
            updatedAt: now,
            overlay: {
                ...descriptor.overlay,
                headPatchSha: committed.overlayHeadPatchSha,
                patchCount: committed.overlayPatchCount,
            },
            intentQueue: {
                nextIntentSeq: intentQueue.nextIntentSeq,
                intents: [],
            },
            evolution: {
                tickCount: tickIndex,
                lastTick: tickRecord,
            },
        };
    }
    _updateTickCaches(maxLamport) {
        if (maxLamport > this._graph._maxObservedLamport) {
            this._graph._maxObservedLamport = maxLamport;
        }
        this._graph._stateDirty = true;
        this._graph._cachedViewHash = null;
        this._graph._cachedCeiling = null;
        this._graph._cachedFrontier = null;
    }
}
