import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { buildContextBlock as formatContextBlock } from "./contextFormat.js";
import { createContextItem, isContextItem, normalizeContextItem } from "./contextItem.js";
export class ContextStore {
    workspace;
    sessionId;
    contextDir;
    logPath;
    snapshotPath;
    constructor(options) {
        this.workspace = path.resolve(options.workspace);
        this.sessionId = options.sessionId;
        this.contextDir = path.join(this.workspace, ".patchpilot", "context");
        this.logPath = path.join(this.contextDir, `${this.sessionId}.jsonl`);
        this.snapshotPath = path.join(this.contextDir, `${this.sessionId}.snapshot.json`);
    }
    static workspaceContextPath(workspace, sessionId) {
        return path.join(path.resolve(workspace), ".patchpilot", "context", `${sessionId}.jsonl`);
    }
    static workspaceSnapshotPath(workspace, sessionId) {
        return path.join(path.resolve(workspace), ".patchpilot", "context", `${sessionId}.snapshot.json`);
    }
    async append(input) {
        const item = isContextItem(input) ? normalizeContextItem(input) : createContextItem(input);
        await this.appendRecord({
            type: "context.item.appended",
            item,
            createdAt: new Date().toISOString()
        });
        return item;
    }
    async loadItems() {
        return (await this.snapshot()).items;
    }
    async snapshot() {
        const records = await this.readRecords();
        const cached = await this.readSnapshot();
        if (cached && cached.eventCount === records.length) {
            return cached;
        }
        const replayed = replayRecords(this.sessionId, records);
        await this.writeSnapshot(replayed.snapshot);
        return replayed.snapshot;
    }
    async pin(itemId) {
        await this.appendRecord({
            type: "context.item.pinned",
            itemId,
            pinned: true,
            createdAt: new Date().toISOString()
        });
    }
    async unpin(itemId) {
        await this.appendRecord({
            type: "context.item.pinned",
            itemId,
            pinned: false,
            createdAt: new Date().toISOString()
        });
    }
    async drop(itemId) {
        await this.appendRecord({
            type: "context.item.dropped",
            itemId,
            createdAt: new Date().toISOString()
        });
    }
    async clear(options = {}) {
        await this.appendRecord({
            type: "context.cleared",
            includePinned: options.includePinned ?? false,
            kinds: options.kinds,
            createdAt: new Date().toISOString()
        });
    }
    async recordSummary(input) {
        return await this.append({
            ...input,
            kind: "summary",
            source: input.source ?? "session",
            priority: input.priority ?? 60
        });
    }
    async resetSummaries() {
        await this.appendRecord({
            type: "context.summaries.reset",
            createdAt: new Date().toISOString()
        });
    }
    async setAutoCompaction(enabled) {
        await this.appendRecord({
            type: "context.auto_compaction.set",
            enabled,
            createdAt: new Date().toISOString()
        });
    }
    async buildContextBlock(options = {}) {
        return formatContextBlock(await this.snapshot(), options);
    }
    async bootstrapFromSession(events) {
        const existingKeys = new Set((await this.loadItems()).map((item) => item.meta?.sessionEventKey).filter((value) => typeof value === "string"));
        const created = [];
        for (const event of events) {
            const input = contextItemFromSessionEvent(event);
            if (!input) {
                continue;
            }
            const sessionEventKey = input?.meta?.sessionEventKey;
            if (typeof sessionEventKey !== "string" || existingKeys.has(sessionEventKey)) {
                continue;
            }
            const item = await this.append(input);
            existingKeys.add(sessionEventKey);
            created.push(item);
        }
        return created;
    }
    async appendRecord(record) {
        await mkdir(this.contextDir, {
            recursive: true
        });
        await appendFile(this.logPath, `${JSON.stringify(record)}\n`, "utf8");
        const replayed = replayRecords(this.sessionId, await this.readRecords());
        await this.writeSnapshot(replayed.snapshot);
    }
    async readRecords() {
        const content = await readFile(this.logPath, "utf8").catch(() => "");
        return content
            .split(/\r?\n/)
            .filter(Boolean)
            .flatMap((line) => {
            try {
                const parsed = JSON.parse(line);
                return isContextLogRecord(parsed) ? [parsed] : [];
            }
            catch {
                return [];
            }
        });
    }
    async readSnapshot() {
        const content = await readFile(this.snapshotPath, "utf8").catch(() => "");
        if (!content.trim()) {
            return null;
        }
        try {
            const parsed = JSON.parse(content);
            return isContextSnapshot(parsed, this.sessionId) ? parsed : null;
        }
        catch {
            return null;
        }
    }
    async writeSnapshot(snapshot) {
        await mkdir(this.contextDir, {
            recursive: true
        });
        await writeFile(this.snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
    }
}
function replayRecords(sessionId, records) {
    const items = new Map();
    let autoCompactionEnabled = false;
    let updatedAt = new Date(0).toISOString();
    for (const record of records) {
        updatedAt = record.createdAt;
        switch (record.type) {
            case "context.item.appended":
                items.set(record.item.id, normalizeContextItem(record.item));
                break;
            case "context.item.pinned":
                patchItem(items, record.itemId, {
                    pinned: record.pinned
                });
                break;
            case "context.item.dropped":
                patchItem(items, record.itemId, {
                    dropped: true
                });
                break;
            case "context.cleared":
                for (const item of items.values()) {
                    if ((record.includePinned || !item.pinned) && (!record.kinds || record.kinds.includes(item.kind))) {
                        item.dropped = true;
                    }
                }
                break;
            case "context.summaries.reset":
                for (const item of items.values()) {
                    if (item.kind === "summary") {
                        item.dropped = true;
                    }
                }
                break;
            case "context.auto_compaction.set":
                autoCompactionEnabled = record.enabled;
                break;
        }
    }
    return {
        recordCount: records.length,
        snapshot: {
            version: 1,
            sessionId,
            updatedAt,
            eventCount: records.length,
            autoCompactionEnabled,
            items: [...items.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt))
        }
    };
}
function patchItem(items, itemId, patch) {
    const item = items.get(itemId);
    if (!item) {
        return;
    }
    items.set(itemId, {
        ...item,
        ...patch
    });
}
function contextItemFromSessionEvent(event) {
    switch (event.type) {
        case "run.started":
            return {
                kind: "turn",
                source: "user",
                label: event.task,
                text: event.task,
                runId: event.runId,
                createdAt: event.startedAt,
                meta: {
                    sessionEventKey: `${event.type}:${event.runId}`
                }
            };
        case "run.completed":
            return {
                kind: "turn",
                source: "assistant",
                label: "assistant response",
                text: event.message,
                runId: event.runId,
                createdAt: event.completedAt,
                meta: {
                    sessionEventKey: `${event.type}:${event.runId}`
                }
            };
        case "tool.completed":
            return {
                kind: "tool_result",
                source: "tool",
                label: `${event.tool} ${event.ok ? "ok" : "failed"}`,
                text: event.summary,
                runId: event.runId,
                createdAt: event.createdAt,
                priority: event.ok ? 20 : 45,
                meta: {
                    sessionEventKey: `${event.type}:${event.toolCallId}`,
                    tool: event.tool,
                    ok: event.ok
                }
            };
        default:
            return null;
    }
}
function isContextSnapshot(value, sessionId) {
    if (!value || typeof value !== "object") {
        return false;
    }
    const candidate = value;
    return (candidate.version === 1 &&
        candidate.sessionId === sessionId &&
        typeof candidate.updatedAt === "string" &&
        typeof candidate.eventCount === "number" &&
        typeof candidate.autoCompactionEnabled === "boolean" &&
        Array.isArray(candidate.items) &&
        candidate.items.every(isContextItem));
}
function isContextLogRecord(value) {
    if (!value || typeof value !== "object") {
        return false;
    }
    const candidate = value;
    if (typeof candidate.type !== "string" || typeof candidate.createdAt !== "string") {
        return false;
    }
    switch (candidate.type) {
        case "context.item.appended":
            return isContextItem(candidate.item);
        case "context.item.pinned":
            return typeof candidate.itemId === "string" && typeof candidate.pinned === "boolean";
        case "context.item.dropped":
            return typeof candidate.itemId === "string";
        case "context.cleared":
            return typeof candidate.includePinned === "boolean" && (candidate.kinds === undefined || Array.isArray(candidate.kinds));
        case "context.summaries.reset":
            return true;
        case "context.auto_compaction.set":
            return typeof candidate.enabled === "boolean";
        default:
            return false;
    }
}
//# sourceMappingURL=contextStore.js.map