import { randomUUID } from "node:crypto";
import { createDocumentStore } from "./state-backend.js";
export class PromptStore {
    store;
    lastPrompts = new Map();
    queues = new Map();
    pausedContexts = new Set();
    drainLocks = new Map();
    constructor(workspace, backend = "json") {
        this.store = createDocumentStore({
            workspace,
            fileName: "prompts.json",
            sqliteKey: "prompts",
            backend,
        });
        this.refresh();
    }
    setLastPrompt(contextKey, prompt) {
        this.updateState((state) => {
            state.lastPrompts.set(contextKey, prompt);
        });
    }
    getLastPrompt(contextKey) {
        this.refresh();
        return this.lastPrompts.get(contextKey);
    }
    enqueue(contextKey, prompt, options = {}) {
        return this.updateState((state) => {
            const idempotencyKey = normalizeQueueIdempotencyKey(options.idempotencyKey ?? prompt.correlationId);
            const queue = state.queues.get(contextKey) ?? [];
            if (idempotencyKey) {
                const existing = queue.find((item) => normalizeQueueIdempotencyKey(item.idempotencyKey) === idempotencyKey);
                if (existing) {
                    return existing;
                }
            }
            const item = {
                ...prompt,
                id: createQueueId(),
                contextKey,
                createdAt: Date.now(),
                status: "queued",
                idempotencyKey,
                notBefore: options.notBefore,
            };
            queue.push(item);
            state.queues.set(contextKey, queue);
            return item;
        });
    }
    enqueueFront(contextKey, prompt) {
        this.updateState((state) => {
            const queue = (state.queues.get(contextKey) ?? []).filter((item) => item.id !== prompt.id);
            queue.unshift(clearPromptLease({ ...prompt, status: "queued", updatedAt: Date.now() }));
            state.queues.set(contextKey, queue);
        });
    }
    dequeue(contextKey) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            if (!queue || queue.length === 0) {
                return undefined;
            }
            const now = Date.now();
            const index = queue.findIndex((queued) => !queued.notBefore || queued.notBefore <= now);
            if (index === -1) {
                return undefined;
            }
            const [item] = queue.splice(index, 1);
            if (queue.length === 0) {
                state.queues.delete(contextKey);
            }
            if (item) {
                item.attempts = (item.attempts ?? 0) + 1;
                item.updatedAt = Date.now();
            }
            return item;
        });
    }
    leaseNext(contextKey, owner, ttlMs) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            if (!queue?.length) {
                return undefined;
            }
            const now = Date.now();
            clearExpiredQueueLeases(queue, now);
            const index = queue.findIndex((queued) => {
                if (queued.status === "running" && (queued.leaseExpiresAt ?? 0) > now) {
                    return false;
                }
                return !queued.notBefore || queued.notBefore <= now;
            });
            if (index === -1) {
                return undefined;
            }
            const item = queue[index];
            const leaseId = createQueueId();
            item.status = "running";
            item.leaseOwner = owner;
            item.leaseId = leaseId;
            item.leaseStartedAt = now;
            item.leaseExpiresAt = now + Math.max(1_000, ttlMs);
            item.attempts = (item.attempts ?? 0) + 1;
            item.updatedAt = now;
            queue.splice(index, 1);
            queue.unshift(item);
            state.queues.set(contextKey, queue);
            return { ...item };
        });
    }
    renewLease(contextKey, item, owner, ttlMs) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            const current = queue?.find((queued) => queued.id === item.id);
            if (!current || !leaseMatches(current, item, owner)) {
                return false;
            }
            const now = Date.now();
            current.leaseExpiresAt = now + Math.max(1_000, ttlMs);
            current.updatedAt = now;
            return true;
        });
    }
    completeLease(contextKey, item, owner) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            if (!queue) {
                return undefined;
            }
            const index = queue.findIndex((queued) => queued.id === item.id);
            if (index === -1) {
                return undefined;
            }
            const current = queue[index];
            if (!leaseMatches(current, item, owner)) {
                return undefined;
            }
            const [removed] = queue.splice(index, 1);
            if (queue.length === 0) {
                state.queues.delete(contextKey);
            }
            return removed;
        });
    }
    failLease(contextKey, item, owner, error, options = {}) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            const current = queue?.find((queued) => queued.id === item.id);
            if (!queue || !current || !leaseMatches(current, item, owner)) {
                return undefined;
            }
            const now = Date.now();
            Object.assign(current, clearPromptLease({
                ...current,
                status: "queued",
                lastError: error,
                updatedAt: now,
                notBefore: options.notBefore ?? current.notBefore,
            }));
            const index = queue.findIndex((queued) => queued.id === current.id);
            if (index > 0) {
                queue.splice(index, 1);
                queue.unshift(current);
            }
            state.queues.set(contextKey, queue);
            return { ...current };
        });
    }
    list(contextKey) {
        this.refresh();
        return [...(this.queues.get(contextKey) ?? [])];
    }
    get(contextKey, id) {
        this.refresh();
        return this.queues.get(contextKey)?.find((item) => item.id === id);
    }
    nextRunnableAt(contextKey) {
        this.refresh();
        const timestamps = (this.queues.get(contextKey) ?? [])
            .map((item) => item.notBefore)
            .filter((value) => typeof value === "number")
            .sort((left, right) => left - right);
        return timestamps[0] ?? null;
    }
    listContextKeys() {
        this.refresh();
        return [...new Set([...this.queues.keys(), ...this.pausedContexts])];
    }
    remove(contextKey, id) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            if (!queue) {
                return undefined;
            }
            const index = queue.findIndex((item) => item.id === id);
            if (index === -1) {
                return undefined;
            }
            const [removed] = queue.splice(index, 1);
            if (queue.length === 0) {
                state.queues.delete(contextKey);
            }
            return removed;
        });
    }
    moveToTop(contextKey, id) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            if (!queue) {
                return undefined;
            }
            const index = queue.findIndex((item) => item.id === id);
            if (index === -1) {
                return undefined;
            }
            const [item] = queue.splice(index, 1);
            item.updatedAt = Date.now();
            queue.unshift(item);
            return item;
        });
    }
    moveUp(contextKey, id) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            if (!queue) {
                return undefined;
            }
            const index = queue.findIndex((item) => item.id === id);
            if (index <= 0) {
                return queue[index];
            }
            const [item] = queue.splice(index, 1);
            queue.splice(index - 1, 0, item);
            item.updatedAt = Date.now();
            return item;
        });
    }
    moveDown(contextKey, id) {
        return this.updateState((state) => {
            const queue = state.queues.get(contextKey);
            if (!queue) {
                return undefined;
            }
            const index = queue.findIndex((item) => item.id === id);
            if (index === -1) {
                return undefined;
            }
            if (index >= queue.length - 1) {
                return queue[index];
            }
            const [item] = queue.splice(index, 1);
            queue.splice(index + 1, 0, item);
            item.updatedAt = Date.now();
            return item;
        });
    }
    markFailed(contextKey, item, error) {
        this.updateState((state) => {
            const queue = (state.queues.get(contextKey) ?? []).filter((queued) => queued.id !== item.id);
            queue.unshift(clearPromptLease({ ...item, status: "queued", lastError: error, updatedAt: Date.now() }));
            state.queues.set(contextKey, queue);
        });
    }
    clear(contextKey) {
        return this.updateState((state) => {
            const count = state.queues.get(contextKey)?.length ?? 0;
            state.queues.delete(contextKey);
            return count;
        });
    }
    pause(contextKey) {
        this.updateState((state) => {
            state.pausedContexts.add(contextKey);
        });
    }
    resume(contextKey) {
        this.updateState((state) => {
            state.pausedContexts.delete(contextKey);
        });
    }
    isPaused(contextKey) {
        this.refresh();
        return this.pausedContexts.has(contextKey);
    }
    acquireDrainLock(contextKey, owner, ttlMs) {
        return this.updateState((state) => {
            const now = Date.now();
            const existing = state.drainLocks.get(contextKey);
            if (existing && existing.owner !== owner && existing.expiresAt > now) {
                return false;
            }
            state.drainLocks.set(contextKey, {
                owner,
                acquiredAt: existing?.owner === owner ? existing.acquiredAt : now,
                updatedAt: now,
                expiresAt: now + Math.max(1_000, ttlMs),
            });
            return true;
        });
    }
    renewDrainLock(contextKey, owner, ttlMs) {
        return this.updateState((state) => {
            const now = Date.now();
            const existing = state.drainLocks.get(contextKey);
            if (!existing || existing.owner !== owner) {
                return false;
            }
            state.drainLocks.set(contextKey, {
                ...existing,
                updatedAt: now,
                expiresAt: now + Math.max(1_000, ttlMs),
            });
            return true;
        });
    }
    releaseDrainLock(contextKey, owner) {
        return this.updateState((state) => {
            const existing = state.drainLocks.get(contextKey);
            if (!existing || existing.owner !== owner) {
                return false;
            }
            state.drainLocks.delete(contextKey);
            return true;
        });
    }
    getDrainLock(contextKey) {
        this.refresh();
        const lock = this.drainLocks.get(contextKey);
        return lock ? { ...lock } : undefined;
    }
    updateState(mutator) {
        let result;
        try {
            const payload = this.store.update((current) => {
                const state = stateFromPayload(current);
                result = mutator(state);
                return payloadFromState(state);
            });
            this.applyState(stateFromPayload(payload));
            return result;
        }
        catch (error) {
            console.warn("Failed to persist prompt store:", error instanceof Error ? error.message : String(error));
            throw error;
        }
    }
    refresh() {
        try {
            this.applyState(stateFromPayload(this.store.read()));
        }
        catch (error) {
            console.warn("Failed to load prompt store:", error instanceof Error ? error.message : String(error));
        }
    }
    applyState(state) {
        this.lastPrompts = state.lastPrompts;
        this.queues = state.queues;
        this.pausedContexts = state.pausedContexts;
        this.drainLocks = state.drainLocks;
    }
}
export function describePromptInput(input) {
    if (typeof input === "string") {
        return trimDescription(input);
    }
    const parts = [];
    if (input.text) {
        parts.push(trimDescription(input.text));
    }
    if (input.imagePaths?.length) {
        parts.push(`${input.imagePaths.length} image${input.imagePaths.length === 1 ? "" : "s"}`);
    }
    if (input.stagedFileInstructions) {
        parts.push("staged file input");
    }
    return parts.join(" · ") || "prompt";
}
export function displayTextForPromptInput(input) {
    if (typeof input === "string") {
        return input;
    }
    return input.text ?? "";
}
export function displayMetaForPromptInput(input) {
    if (typeof input === "string") {
        return [];
    }
    const meta = [];
    if (input.imagePaths?.length) {
        meta.push(`${input.imagePaths.length} image${input.imagePaths.length === 1 ? "" : "s"}`);
    }
    if (input.stagedFileInstructions) {
        meta.push("staged file input");
    }
    return meta;
}
export function displayTextForPromptEnvelope(envelope) {
    const text = envelope.displayText ?? displayTextForPromptInput(envelope.input);
    return text || envelope.description;
}
export function displayMetaForPromptEnvelope(envelope) {
    return envelope.displayMeta ?? displayMetaForPromptInput(envelope.input);
}
export function toPromptEnvelope(input, artifactOutDir) {
    return {
        input,
        artifactOutDir,
        description: describePromptInput(input),
        displayText: displayTextForPromptInput(input),
        displayMeta: displayMetaForPromptInput(input),
    };
}
export function webChatAttachmentsForStagedFiles(files, turnId) {
    return files.map((file) => ({
        id: file.safeName,
        kind: attachmentKind(file.mimeType),
        name: file.safeName,
        mimeType: file.mimeType,
        sizeBytes: file.sizeBytes,
        turnId,
    }));
}
export function createCorrelationId() {
    return randomUUID().replace(/-/g, "").slice(0, 12);
}
export function ensurePromptCorrelationId(prompt) {
    const existing = prompt.correlationId?.trim();
    if (existing) {
        return { ...prompt, correlationId: existing };
    }
    return { ...prompt, correlationId: createCorrelationId() };
}
function createQueueId() {
    return randomUUID().replace(/-/g, "").slice(0, 8);
}
function normalizeQueueIdempotencyKey(value) {
    const normalized = value?.trim();
    return normalized || undefined;
}
function clearPromptLease(item) {
    delete item.leaseId;
    delete item.leaseOwner;
    delete item.leaseStartedAt;
    delete item.leaseExpiresAt;
    return item;
}
function clearExpiredQueueLeases(queue, now) {
    for (const item of queue) {
        if (item.status === "running" && (item.leaseExpiresAt ?? 0) <= now) {
            clearPromptLease(item);
            item.status = "queued";
            item.updatedAt = now;
            item.lastError = item.lastError ?? "Queue lease expired before completion.";
        }
    }
}
function leaseMatches(current, expected, owner) {
    return Boolean(current.leaseId &&
        expected.leaseId &&
        current.leaseId === expected.leaseId &&
        current.leaseOwner === owner &&
        expected.leaseOwner === owner);
}
function stateFromPayload(payload) {
    const state = {
        lastPrompts: new Map(),
        queues: new Map(),
        pausedContexts: new Set(),
        drainLocks: new Map(),
    };
    if (!payload) {
        return state;
    }
    for (const [contextKey, prompt] of Object.entries(payload.lastPrompts ?? {})) {
        if (isPromptEnvelope(prompt)) {
            state.lastPrompts.set(contextKey, prompt);
        }
    }
    for (const [contextKey, queue] of Object.entries(payload.queues ?? {})) {
        if (Array.isArray(queue)) {
            const items = queue.filter(isQueuedPrompt);
            if (items.length > 0) {
                state.queues.set(contextKey, items);
            }
        }
    }
    if (Array.isArray(payload.pausedContexts)) {
        state.pausedContexts = new Set(payload.pausedContexts.filter((contextKey) => typeof contextKey === "string"));
    }
    for (const [contextKey, lock] of Object.entries(payload.drainLocks ?? {})) {
        if (isQueueDrainLock(lock)) {
            state.drainLocks.set(contextKey, lock);
        }
    }
    return state;
}
function payloadFromState(state) {
    return {
        lastPrompts: Object.fromEntries(state.lastPrompts.entries()),
        queues: Object.fromEntries([...state.queues.entries()].filter(([, queue]) => queue.length > 0)),
        pausedContexts: [...state.pausedContexts],
        drainLocks: Object.fromEntries(state.drainLocks.entries()),
    };
}
function isQueueDrainLock(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return false;
    }
    const candidate = value;
    return typeof candidate.owner === "string" &&
        typeof candidate.acquiredAt === "number" &&
        typeof candidate.updatedAt === "number" &&
        typeof candidate.expiresAt === "number";
}
function isPromptEnvelope(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return false;
    }
    const candidate = value;
    return isCodexPromptInput(candidate.input) &&
        typeof candidate.description === "string" &&
        (candidate.displayText === undefined || typeof candidate.displayText === "string") &&
        (candidate.displayMeta === undefined ||
            (Array.isArray(candidate.displayMeta) && candidate.displayMeta.every((item) => typeof item === "string"))) &&
        (candidate.attachments === undefined ||
            (Array.isArray(candidate.attachments) && candidate.attachments.every(isWebChatAttachmentLike))) &&
        (candidate.correlationId === undefined || typeof candidate.correlationId === "string");
}
function isWebChatAttachmentLike(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return false;
    }
    const candidate = value;
    return typeof candidate.id === "string" &&
        typeof candidate.name === "string" &&
        typeof candidate.mimeType === "string" &&
        typeof candidate.sizeBytes === "number" &&
        typeof candidate.turnId === "string" &&
        ["image", "audio", "file"].includes(candidate.kind);
}
function isQueuedPrompt(value) {
    return isPromptEnvelope(value) &&
        typeof value.id === "string" &&
        typeof value.contextKey === "string" &&
        typeof value.createdAt === "number" &&
        (value.status === undefined || value.status === "queued" || value.status === "running") &&
        (value.notBefore === undefined || typeof value.notBefore === "number") &&
        (value.updatedAt === undefined || typeof value.updatedAt === "number") &&
        (value.attempts === undefined || typeof value.attempts === "number") &&
        (value.lastError === undefined || typeof value.lastError === "string") &&
        (value.idempotencyKey === undefined || typeof value.idempotencyKey === "string") &&
        (value.leaseId === undefined || typeof value.leaseId === "string") &&
        (value.leaseOwner === undefined || typeof value.leaseOwner === "string") &&
        (value.leaseStartedAt === undefined || typeof value.leaseStartedAt === "number") &&
        (value.leaseExpiresAt === undefined || typeof value.leaseExpiresAt === "number");
}
function isCodexPromptInput(value) {
    if (typeof value === "string") {
        return true;
    }
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return false;
    }
    const candidate = value;
    return ((candidate.text === undefined || typeof candidate.text === "string") &&
        (candidate.stagedFileInstructions === undefined || typeof candidate.stagedFileInstructions === "string") &&
        (candidate.imagePaths === undefined ||
            (Array.isArray(candidate.imagePaths) && candidate.imagePaths.every((item) => typeof item === "string"))));
}
function trimDescription(text) {
    const singleLine = text.replace(/\s+/g, " ").trim();
    return singleLine.length <= 80 ? singleLine : `${singleLine.slice(0, 79)}…`;
}
function attachmentKind(mimeType) {
    if (mimeType.startsWith("image/"))
        return "image";
    if (mimeType.startsWith("audio/"))
        return "audio";
    return "file";
}
