import { randomUUID } from "node:crypto";
import { activityActorLabel, activityCategoryForType, } from "../core/activity-events.js";
import { cursorPage, normalizeCursorLimit } from "../core/pagination.js";
import { createDocumentStore } from "../state/state-backend.js";
const DEFAULT_CHAT_LIMIT = 300;
const DEFAULT_ACTIVITY_LIMIT = 1000;
const CROSS_SOURCE_AGENT_DEDUPE_MS = 10 * 60 * 1000;
const CROSS_SOURCE_AGENT_DEDUPE_MIN_CHARS = 120;
export class WebChatStore {
    store;
    maxMessages;
    constructor(workspace, backend = "json", maxMessages = DEFAULT_CHAT_LIMIT) {
        this.store = createDocumentStore({
            workspace,
            fileName: "web-chat.json",
            sqliteKey: "web-chat",
            backend,
        });
        this.maxMessages = maxMessages;
    }
    append(input) {
        return this.appendWithResult(input).message;
    }
    appendWithResult(input) {
        const threadId = input.threadId || "pending";
        let result;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const messages = payload.messagesByThread[threadId] ?? [];
            const duplicate = findDuplicateWebChatMessage(messages, { ...input, threadId });
            if (duplicate) {
                result = { message: duplicate, inserted: false };
                return payload;
            }
            const message = {
                id: randomId(),
                timestamp: input.timestamp ?? new Date().toISOString(),
                ...input,
                threadId,
            };
            messages.push(message);
            if (messages.length > this.maxMessages) {
                messages.splice(0, messages.length - this.maxMessages);
            }
            payload.messagesByThread[threadId] = messages;
            result = { message, inserted: true };
            return payload;
        });
        return result;
    }
    upsertByKey(input) {
        const threadId = input.threadId || "pending";
        const now = new Date().toISOString();
        let result;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const messages = payload.messagesByThread[threadId] ?? [];
            const existing = messages.find((message) => message.key === input.key);
            if (existing) {
                existing.role = input.role;
                existing.text = input.text;
                existing.source = input.source;
                existing.correlationId = input.correlationId;
                existing.turnId = input.turnId;
                existing.timestamp = input.timestamp ?? now;
                existing.key = input.key;
                if (input.meta !== undefined) {
                    existing.meta = input.meta;
                }
                existing.actionResolution = input.actionResolution ?? existing.actionResolution;
                existing.actions = filterResolvedWebChatActions(input.actions, existing.actionResolution);
                result = { message: existing, inserted: false, updated: true };
                return payload;
            }
            const message = {
                id: randomId(),
                timestamp: input.timestamp ?? now,
                ...input,
                threadId,
            };
            messages.push(message);
            if (messages.length > this.maxMessages) {
                messages.splice(0, messages.length - this.maxMessages);
            }
            payload.messagesByThread[threadId] = messages;
            result = { message, inserted: true, updated: false };
            return payload;
        });
        return result;
    }
    resolveAction(input) {
        const actionId = input.actionId.trim();
        if (!actionId) {
            return 0;
        }
        const actionPrefix = input.actionPrefix?.trim();
        const actionSuffix = `:${actionId}`;
        const resolvedAt = input.resolvedAt ?? new Date().toISOString();
        let updated = 0;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            for (const [threadId, messages] of Object.entries(payload.messagesByThread)) {
                if (input.threadId && threadId !== input.threadId) {
                    continue;
                }
                for (const message of messages) {
                    const actions = message.actions ?? [];
                    if (!actions.some((action) => isMatchingWebChatAction(action, actionSuffix, actionPrefix))) {
                        continue;
                    }
                    const remaining = actions.filter((action) => !isMatchingWebChatAction(action, actionSuffix, actionPrefix));
                    message.actions = remaining.length ? remaining : undefined;
                    message.actionResolution = { actionId, label: input.label, resolvedAt };
                    updated += 1;
                }
            }
            return payload;
        });
        return updated;
    }
    list(threadId, limit = 200) {
        const messages = this.readPayload().messagesByThread[threadId || "pending"] ?? [];
        return messages.slice(-Math.max(1, Math.min(this.maxMessages, limit)));
    }
    listPage(threadId, options = {}) {
        const limit = normalizeCursorLimit(options.limit, 80, this.maxMessages);
        const messages = this.readPayload().messagesByThread[threadId || "pending"] ?? [];
        const newestFirst = [...messages].sort((left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp));
        const page = cursorPage(newestFirst, options.cursor, limit, (message) => message.id);
        return {
            items: page.items.sort((left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp)),
            pagination: page.pagination,
        };
    }
    findByCorrelationId(correlationId, limit = 100) {
        const needle = correlationId.trim();
        if (!needle) {
            return [];
        }
        return Object.values(this.readPayload().messagesByThread)
            .flat()
            .filter((message) => message.correlationId === needle)
            .sort((left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp))
            .slice(-Math.max(1, Math.min(this.maxMessages, limit)));
    }
    clear(threadId) {
        const key = threadId || "pending";
        let count = 0;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            count = payload.messagesByThread[key]?.length ?? 0;
            delete payload.messagesByThread[key];
            return payload;
        });
        return count;
    }
    readPayload() {
        return this.normalizePayload(this.store.read());
    }
    normalizePayload(payload) {
        if (!payload || payload.version !== 1 || !payload.messagesByThread || typeof payload.messagesByThread !== "object") {
            return { version: 1, messagesByThread: {} };
        }
        const messagesByThread = {};
        for (const [threadId, messages] of Object.entries(payload.messagesByThread)) {
            if (Array.isArray(messages)) {
                messagesByThread[threadId] = dedupeWebChatMessages(messages.filter(isWebChatMessage)).slice(-this.maxMessages);
            }
        }
        return { version: 1, messagesByThread };
    }
}
export class WebActivityStore {
    store;
    maxEvents;
    constructor(workspace, backend = "json", maxEvents = DEFAULT_ACTIVITY_LIMIT) {
        this.store = createDocumentStore({
            workspace,
            fileName: "web-activity.json",
            sqliteKey: "web-activity",
            backend,
        });
        this.maxEvents = maxEvents;
    }
    append(input) {
        const event = {
            id: randomId(),
            timestamp: input.timestamp ?? new Date().toISOString(),
            ...input,
            category: input.category ?? activityCategoryForType(input.type),
        };
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            payload.events.push(event);
            if (payload.events.length > this.maxEvents) {
                payload.events.splice(0, payload.events.length - this.maxEvents);
            }
            return payload;
        });
        return event;
    }
    list(options = {}) {
        return this.listPage(options).items;
    }
    listPage(options = {}) {
        const limit = normalizeCursorLimit(options.limit, 100, 500);
        const events = this.filteredEvents(options)
            .sort((left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp));
        return cursorPage(events, options.cursor, limit, (event) => event.id);
    }
    findByCorrelationId(correlationId, limit = 100) {
        const needle = correlationId.trim();
        if (!needle) {
            return [];
        }
        return this.readPayload().events
            .filter((event) => event.correlationId === needle)
            .sort((left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp))
            .slice(-Math.max(1, Math.min(500, limit)));
    }
    filteredEvents(options) {
        const since = normalizeSince(options.since);
        return this.readPayload().events
            .filter((event) => !options.source || options.source === "all" || event.source === options.source)
            .filter((event) => !options.status || options.status === "all" || event.status === options.status)
            .filter((event) => !options.category || options.category === "all" || (event.category ?? activityCategoryForType(event.type)) === options.category)
            .filter((event) => !options.agentId || options.agentId === "all" || event.agentId === options.agentId)
            .filter((event) => !options.threadId || event.threadId === options.threadId)
            .filter((event) => !options.workspace || event.workspace === options.workspace)
            .filter((event) => !options.type || event.type.toLowerCase().includes(options.type.toLowerCase()))
            .filter((event) => !options.actor || activityActorMatches(event.actor, options.actor))
            .filter((event) => !since || Date.parse(event.timestamp) >= since);
    }
    readPayload() {
        return this.normalizePayload(this.store.read());
    }
    normalizePayload(payload) {
        if (!payload || payload.version !== 1 || !Array.isArray(payload.events)) {
            return { version: 1, events: [] };
        }
        return {
            version: 1,
            events: payload.events.filter(isWebActivityEvent).slice(-this.maxEvents),
        };
    }
}
function isWebChatMessage(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return false;
    }
    const candidate = value;
    return typeof candidate.id === "string" &&
        typeof candidate.threadId === "string" &&
        typeof candidate.text === "string" &&
        typeof candidate.timestamp === "string" &&
        (candidate.meta === undefined || (Array.isArray(candidate.meta) && candidate.meta.every((item) => typeof item === "string"))) &&
        (candidate.attachments === undefined ||
            (Array.isArray(candidate.attachments) && candidate.attachments.every(isWebChatAttachment))) &&
        (candidate.key === undefined || typeof candidate.key === "string") &&
        (candidate.actions === undefined || (Array.isArray(candidate.actions) && candidate.actions.every(isWebChatAction))) &&
        (candidate.actionResolution === undefined || isWebChatActionResolution(candidate.actionResolution)) &&
        ["user", "agent", "system", "tool"].includes(candidate.role) &&
        ["web", "telegram", "discord", "slack", "matrix", "cli"].includes(candidate.source);
}
function isWebChatAttachment(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 isWebChatAction(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return false;
    }
    const candidate = value;
    return typeof candidate.label === "string" &&
        typeof candidate.action === "string" &&
        (candidate.style === undefined || ["primary", "secondary", "danger"].includes(candidate.style)) &&
        (candidate.title === undefined || typeof candidate.title === "string");
}
function isWebChatActionResolution(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return false;
    }
    const candidate = value;
    return typeof candidate.actionId === "string" &&
        typeof candidate.label === "string" &&
        typeof candidate.resolvedAt === "string";
}
function filterResolvedWebChatActions(actions, resolution) {
    if (!actions?.length) {
        return undefined;
    }
    if (!resolution?.actionId) {
        return actions;
    }
    const suffix = `:${resolution.actionId}`;
    const filtered = actions.filter((action) => !action.action.endsWith(suffix));
    return filtered.length ? filtered : undefined;
}
function isMatchingWebChatAction(action, actionSuffix, actionPrefix) {
    return action.action.endsWith(actionSuffix) && (!actionPrefix || action.action.startsWith(actionPrefix));
}
function isWebActivityEvent(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return false;
    }
    const candidate = value;
    return typeof candidate.id === "string" &&
        typeof candidate.timestamp === "string" &&
        typeof candidate.type === "string" &&
        ["web", "telegram", "discord", "slack", "matrix", "cli"].includes(candidate.source) &&
        ["queued", "running", "completed", "failed", "aborted", "info"].includes(candidate.status);
}
function normalizeSince(value) {
    if (value === undefined || value === null || value === "") {
        return null;
    }
    const time = typeof value === "number" ? value : Date.parse(value);
    return Number.isFinite(time) ? time : null;
}
function activityActorMatches(actor, query) {
    const needle = query.trim().toLowerCase();
    if (!needle) {
        return true;
    }
    return [
        activityActorLabel(actor),
        actor?.id,
        actor?.username,
        actor?.channelUserId,
        actor?.channel,
    ].some((value) => String(value ?? "").toLowerCase().includes(needle));
}
function randomId() {
    return randomUUID().replace(/-/g, "").slice(0, 12);
}
function dedupeWebChatMessages(messages) {
    const seen = new Set();
    const deduped = [];
    for (const message of messages) {
        const key = webChatDedupKey(message);
        if (key && seen.has(key)) {
            continue;
        }
        const duplicate = findOverlappingAgentDuplicate(deduped, message);
        if (duplicate) {
            if (shouldPreferWebChatMessage(message, duplicate)) {
                const index = deduped.indexOf(duplicate);
                if (index >= 0) {
                    deduped[index] = message;
                }
            }
            continue;
        }
        if (key) {
            seen.add(key);
        }
        deduped.push(message);
    }
    return deduped;
}
function findDuplicateWebChatMessage(messages, input) {
    const key = webChatDedupKey(input);
    const exact = key ? messages.find((message) => webChatDedupKey(message) === key) : undefined;
    return exact ?? findOverlappingAgentDuplicate(messages, input);
}
function findOverlappingAgentDuplicate(messages, input) {
    if (input.role !== "agent") {
        return undefined;
    }
    return [...messages].reverse().find((message) => message.role === "agent" &&
        message.threadId === (input.threadId || "pending") &&
        isComparableAgentDuplicateSource(message, input) &&
        isRecentWebChatDuplicate(message.timestamp, input.timestamp) &&
        hasOverlappingAgentText(message.text, input.text));
}
function isComparableAgentDuplicateSource(left, right) {
    if (left.source !== right.source) {
        return true;
    }
    return Boolean((left.correlationId && right.correlationId && left.correlationId === right.correlationId) ||
        (left.turnId && right.turnId && left.turnId === right.turnId));
}
function isRecentWebChatDuplicate(leftTimestamp, rightTimestamp) {
    const left = leftTimestamp ? Date.parse(leftTimestamp) : Number.NaN;
    const right = rightTimestamp ? Date.parse(rightTimestamp) : Date.now();
    return !Number.isFinite(left) || !Number.isFinite(right) || Math.abs(left - right) <= CROSS_SOURCE_AGENT_DEDUPE_MS;
}
function hasOverlappingAgentText(left, right) {
    const normalizedLeft = normalizeAgentChatText(left);
    const normalizedRight = normalizeAgentChatText(right);
    const shorter = normalizedLeft.length <= normalizedRight.length ? normalizedLeft : normalizedRight;
    const longer = normalizedLeft.length > normalizedRight.length ? normalizedLeft : normalizedRight;
    return shorter.length >= CROSS_SOURCE_AGENT_DEDUPE_MIN_CHARS && (shorter === longer || longer.includes(shorter));
}
function normalizeAgentChatText(text) {
    return text.replace(/\s+/g, " ").trim();
}
function shouldPreferWebChatMessage(candidate, existing) {
    if (existing.source === "cli" && candidate.source !== "cli") {
        return true;
    }
    if (candidate.source === existing.source) {
        return candidate.text.length > existing.text.length;
    }
    return false;
}
function webChatDedupKey(message) {
    const threadId = message.threadId || "pending";
    if (message.key) {
        return [threadId, message.key].join("\0");
    }
    if (message.turnId) {
        return [threadId, message.role, message.source, message.turnId, message.text].join("\0");
    }
    if (message.timestamp) {
        return [threadId, message.role, message.source, message.timestamp, message.text].join("\0");
    }
    return null;
}
export { activityActorLabel, activityCategoryForType, auditCategoryForAction, } from "../core/activity-events.js";
