import { createHash } from "node:crypto";
import { readFileSync, statSync } from "node:fs";
import { BestAgentError } from "@best-agent/foundation";
export class WorkspaceState {
    #diagnostics = new Map();
    #fileStates = new Map();
    #revisions = new Map();
    #snapshots = new Map();
    #writeLocks = new Map();
    recordRead(threadId, path) {
        const currentRevision = this.#revisions.get(path) ?? 0;
        this.#getSnapshots(threadId).set(path, this.#readWorkspaceSnapshot(path, "read", currentRevision));
        this.#ensureDiagnostics(threadId).fullReadCount += 1;
    }
    assertMutationAllowed(threadId, path) {
        const snapshots = this.#snapshots.get(threadId);
        const snapshot = snapshots?.get(path);
        const currentRevision = this.#revisions.get(path) ?? 0;
        const state = this.#readWorkspaceFileState(path);
        if (!state.exists) {
            if (!snapshot || !snapshot.exists) {
                return;
            }
            throw this.#createStaleViewError(threadId, {
                currentRevision,
                path,
                reason: "Path no longer exists. Re-read it before editing.",
                seenRevision: snapshot.revision,
            });
        }
        if (!snapshot) {
            throw this.#createStaleViewError(threadId, {
                currentRevision,
                path,
                reason: "Existing files must be read before they are modified.",
            });
        }
        if (!snapshot.isFullRead) {
            throw this.#createStaleViewError(threadId, {
                currentRevision,
                path,
                reason: "Existing files require a full read before they are modified.",
                seenRevision: snapshot.revision,
            });
        }
        if (snapshot.revision !== currentRevision) {
            throw this.#createStaleViewError(threadId, {
                currentRevision,
                path,
                reason: "Path changed after it was last read. Re-read it before editing.",
                seenRevision: snapshot.revision,
            });
        }
        if (snapshot.mtimeMs !== state.mtimeMs ||
            snapshot.size !== state.size ||
            snapshot.contentHash !== state.contentHash) {
            throw this.#createStaleViewError(threadId, {
                currentRevision,
                path,
                reason: "Path changed on disk after it was last read. Re-read it before editing.",
                seenRevision: snapshot.revision,
            });
        }
    }
    promoteWrite(threadId, path) {
        const nextRevision = (this.#revisions.get(path) ?? 0) + 1;
        this.#revisions.set(path, nextRevision);
        const invalidatedThreadIds = [];
        for (const [candidateThreadId, snapshots] of this.#snapshots.entries()) {
            if (candidateThreadId === threadId) {
                continue;
            }
            if (snapshots.delete(path)) {
                invalidatedThreadIds.push(candidateThreadId);
                this.#ensureDiagnostics(candidateThreadId).invalidationCount += 1;
            }
        }
        this.#fileStates.delete(path);
        this.#getSnapshots(threadId).set(path, this.#readWorkspaceSnapshot(path, "write", nextRevision));
        this.#ensureDiagnostics(threadId).promotedWriteCount += 1;
        return {
            invalidatedThreadIds,
            revision: nextRevision,
        };
    }
    async withWriteLocks(paths, action) {
        const normalizedPaths = Array.from(new Set(paths.map((path) => path.trim()).filter(Boolean))).sort();
        if (normalizedPaths.length === 0) {
            return action();
        }
        const releases = [];
        try {
            for (const path of normalizedPaths) {
                releases.push(await this.#acquireWriteLock(path));
            }
            return await action();
        }
        finally {
            for (const release of releases.reverse()) {
                release();
            }
        }
    }
    getDiagnostics(threadId) {
        const diagnostics = this.#diagnostics.get(threadId);
        return {
            fullReadCount: diagnostics?.fullReadCount ?? 0,
            invalidationCount: diagnostics?.invalidationCount ?? 0,
            promotedWriteCount: diagnostics?.promotedWriteCount ?? 0,
            staleWriteRejectedCount: diagnostics?.staleWriteRejectedCount ?? 0,
        };
    }
    #createStaleViewError(threadId, metadata) {
        this.#ensureDiagnostics(threadId).staleWriteRejectedCount += 1;
        const error = new BestAgentError("WORKSPACE_STALE_VIEW", `Workspace path ${metadata.path} is stale. ${metadata.reason}`);
        return Object.assign(error, { workspace: metadata });
    }
    async #acquireWriteLock(path) {
        const previous = this.#writeLocks.get(path) ?? Promise.resolve();
        let resolveCurrent;
        const current = new Promise((resolve) => {
            resolveCurrent = resolve;
        });
        this.#writeLocks.set(path, previous.then(() => current));
        await previous;
        let released = false;
        return () => {
            if (released) {
                return;
            }
            released = true;
            resolveCurrent();
            if (this.#writeLocks.get(path) === current) {
                this.#writeLocks.delete(path);
            }
        };
    }
    #ensureDiagnostics(threadId) {
        let diagnostics = this.#diagnostics.get(threadId);
        if (!diagnostics) {
            diagnostics = {
                fullReadCount: 0,
                invalidationCount: 0,
                promotedWriteCount: 0,
                staleWriteRejectedCount: 0,
            };
            this.#diagnostics.set(threadId, diagnostics);
        }
        return diagnostics;
    }
    #getSnapshots(threadId) {
        let snapshots = this.#snapshots.get(threadId);
        if (!snapshots) {
            snapshots = new Map();
            this.#snapshots.set(threadId, snapshots);
        }
        return snapshots;
    }
    #readWorkspaceSnapshot(path, source, revision) {
        const state = this.#readWorkspaceFileState(path);
        return {
            contentHash: state.contentHash,
            exists: state.exists,
            isFullRead: true,
            mtimeMs: state.mtimeMs,
            revision,
            size: state.size,
            source,
        };
    }
    #readWorkspaceFileState(path) {
        try {
            const stat = statSync(path);
            if (!stat.isFile()) {
                this.#fileStates.delete(path);
                return {
                    contentHash: null,
                    exists: false,
                    mtimeMs: null,
                    size: null,
                };
            }
            const cached = this.#fileStates.get(path);
            if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
                return cached;
            }
            const content = readFileSync(path);
            const nextState = {
                contentHash: createHash("sha1").update(content).digest("hex"),
                exists: true,
                mtimeMs: stat.mtimeMs,
                size: stat.size,
            };
            this.#fileStates.set(path, nextState);
            return nextState;
        }
        catch {
            this.#fileStates.delete(path);
            return {
                contentHash: null,
                exists: false,
                mtimeMs: null,
                size: null,
            };
        }
    }
}
export function getWorkspaceStaleViewErrorMetadata(error) {
    if (!(error instanceof BestAgentError) || error.code !== "WORKSPACE_STALE_VIEW") {
        return undefined;
    }
    return error.workspace;
}
//# sourceMappingURL=workspace-state.js.map