import { and, desc, eq, inArray, isNull, or } from "drizzle-orm";
import { activityLog, approvalComments, approvals, agents, authUsers, heartbeatRuns, issueComments, issues, joinRequests, messengerThreadUserStates, } from "@rudderhq/db";
import { formatMessengerPreview, formatMessengerTitle, } from "@rudderhq/shared";
import { issueService } from "./issues.js";
import { chatService } from "./chats.js";
import { budgetService } from "./budgets.js";
import { redactEventPayload } from "../redaction.js";
const ISSUE_ACTIVITY_ACTIONS = [
    "issue.updated",
    "issue.approval_linked",
    "issue.work_product_created",
    "issue.work_product_updated",
    "issue.work_product_deleted",
    "issue.document_deleted",
    "issue.attachment_added",
    "issue.attachment_removed",
    "heartbeat.cancelled",
    "heartbeat.retried",
];
const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending"]);
const ISSUE_UPDATE_METADATA_KEYS = new Set([
    "identifier",
    "issueIdentifier",
    "_previous",
    "source",
    "reopened",
    "reopenedFrom",
    "normalizedFromStatus",
    "normalizedReason",
]);
function issueUpdatedChangedKeys(details) {
    if (!details)
        return [];
    return Object.keys(details).filter((key) => !ISSUE_UPDATE_METADATA_KEYS.has(key));
}
function isDescriptionOnlyIssueUpdate(activity) {
    if (activity.action !== "issue.updated")
        return false;
    const changedKeys = issueUpdatedChangedKeys(activity.details);
    return changedKeys.length === 1 && changedKeys[0] === "description";
}
function shouldNotifyIssueActivity(activity) {
    if (isDescriptionOnlyIssueUpdate(activity))
        return false;
    return true;
}
function truncate(value, max = 140) {
    return formatMessengerPreview(value, { max });
}
function normalizeDate(value) {
    if (!value)
        return null;
    if (value instanceof Date)
        return Number.isNaN(value.getTime()) ? null : value;
    const parsed = new Date(value);
    return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function maxDate(...values) {
    const dates = values.map(normalizeDate).filter((value) => Boolean(value));
    if (dates.length === 0)
        return null;
    return dates.sort((a, b) => b.getTime() - a.getTime())[0] ?? null;
}
function compareLatestActivity(a, b) {
    const aTime = a.latestActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
    const bTime = b.latestActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
    if (aTime !== bTime)
        return bTime - aTime;
    return a.title.localeCompare(b.title);
}
function compareChronologicalActivity(a, b) {
    const aTime = a.latestActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
    const bTime = b.latestActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
    if (aTime !== bTime)
        return aTime - bTime;
    return a.title.localeCompare(b.title);
}
function threadKeyForChat(conversationId) {
    return `chat:${conversationId}`;
}
function buildAction(label, href, method = null) {
    return { label, href, method };
}
function issueHref(issue) {
    return `/issues/${issue.identifier ?? issue.id}`;
}
function issueDisplayLabel(issue) {
    return issue.identifier ? `${issue.identifier} · ${issue.title}` : issue.title;
}
function issueThreadPreview(issue, preview) {
    const label = issueDisplayLabel(issue);
    const normalizedPreview = truncate(preview, 120);
    if (!normalizedPreview || normalizedPreview === label)
        return truncate(label, 180);
    return truncate(`${label} — ${normalizedPreview}`, 180);
}
function humanizeIssueStatus(status) {
    return status.replaceAll("_", " ");
}
function issueStatusChangeFromActivity(activity) {
    if (!activity || activity.action !== "issue.updated")
        return null;
    const details = activity.details ?? {};
    if (typeof details.status !== "string")
        return null;
    const previous = details._previous && typeof details._previous === "object"
        ? details._previous
        : null;
    const from = typeof previous?.status === "string" ? previous.status : null;
    return { from, to: details.status };
}
function issueStatusActivityMatchesSourceComment(activity, sourceComment) {
    if (!activity || !sourceComment)
        return false;
    const details = activity.details ?? {};
    if (details.source !== "comment")
        return false;
    if (!issueStatusChangeFromActivity(activity))
        return false;
    const activityAt = normalizeDate(activity.createdAt)?.getTime();
    const commentAt = normalizeDate(sourceComment.createdAt)?.getTime();
    if (activityAt === undefined || commentAt === undefined)
        return false;
    return Math.abs(commentAt - activityAt) <= 5_000;
}
function issueBodyFromSnapshot(issue, latestPreview, followed, created, assigned, reviewer) {
    const flags = [];
    if (followed)
        flags.push("followed");
    if (created)
        flags.push("created by me");
    if (assigned)
        flags.push("assigned to me");
    if (reviewer)
        flags.push("review requested");
    const status = issue.status.replaceAll("_", " ");
    const priority = issue.priority.replaceAll("_", " ");
    const prefix = [status, priority].filter(Boolean).join(" · ");
    const suffix = flags.length > 0 ? ` · ${flags.join(" · ")}` : "";
    return latestPreview ?? `${prefix}${suffix}`;
}
function summarizeIssueActivity(activity, issue) {
    const details = activity.details ?? {};
    switch (activity.action) {
        case "issue.updated": {
            if (typeof details.status === "string") {
                const status = humanizeIssueStatus(details.status);
                if (details.status === "done")
                    return "Completed";
                if (details.status === "cancelled")
                    return "Cancelled";
                return `Status changed to ${status}`;
            }
            if (typeof details.assigneeUserId !== "undefined" || typeof details.assigneeAgentId !== "undefined") {
                return "Assignment changed";
            }
            if (typeof details.reviewerUserId !== "undefined" || typeof details.reviewerAgentId !== "undefined") {
                return "Reviewer changed";
            }
            return "Issue updated";
        }
        case "issue.approval_linked":
            return "Approval linked";
        case "issue.work_product_created":
            return "Work product created";
        case "issue.work_product_updated":
            return "Work product updated";
        case "issue.work_product_deleted":
            return "Work product removed";
        case "issue.attachment_added":
            return "Attachment added";
        case "issue.attachment_removed":
            return "Attachment removed";
        case "issue.document_deleted":
            return "Document removed";
        case "heartbeat.cancelled":
            return "Run cancelled";
        case "heartbeat.retried":
            return "Run retried";
        default:
            return `${issue.title} updated`;
    }
}
function isSelfAuthoredComment(comment, userId) {
    return comment.authorUserId === userId;
}
function issueCommentAuthorLabel(comment, currentUserId) {
    if (!comment)
        return null;
    if (comment.authorAgentId)
        return comment.authorAgentName?.trim() || `Agent ${comment.authorAgentId.slice(0, 8)}`;
    if (comment.authorUserId) {
        if (currentUserId && comment.authorUserId === currentUserId)
            return "You";
        return comment.authorUserName?.trim() || `User ${comment.authorUserId.slice(0, 8)}`;
    }
    return "System";
}
function isSelfAuthoredActivity(activity, userId) {
    return activity.actorType === "user" && activity.actorId === userId;
}
function summarizeApprovalPayload(approval) {
    const payload = redactEventPayload(approval.payload);
    if (!payload)
        return null;
    if (approval.type === "chat_issue_creation") {
        const proposal = payload.proposedIssue &&
            typeof payload.proposedIssue === "object" &&
            !Array.isArray(payload.proposedIssue)
            ? payload.proposedIssue
            : payload;
        const title = typeof proposal.title === "string" && proposal.title.trim() ? proposal.title.trim() : null;
        const description = typeof proposal.description === "string" && proposal.description.trim()
            ? truncate(proposal.description.trim(), 120)
            : null;
        return [title ? `Issue: ${title}` : "Agent proposed an issue from chat", description]
            .filter(Boolean)
            .join(" · ");
    }
    if (approval.type === "chat_operation") {
        const proposal = payload.operationProposal &&
            typeof payload.operationProposal === "object" &&
            !Array.isArray(payload.operationProposal)
            ? payload.operationProposal
            : payload;
        const summary = typeof proposal.summary === "string" && proposal.summary.trim() ? proposal.summary.trim() : null;
        return summary ? `Operation: ${truncate(summary, 120)}` : "Agent proposed a chat operation";
    }
    if (approval.type === "hire_agent") {
        const name = typeof payload.name === "string" ? payload.name : null;
        const role = typeof payload.role === "string" ? payload.role : null;
        if (name || role) {
            return [name, role].filter(Boolean).join(" · ");
        }
    }
    if (approval.type === "budget_override_required") {
        const scopeName = typeof payload.scopeName === "string" ? payload.scopeName : null;
        const budgetAmount = typeof payload.budgetAmount === "number" ? `$${(payload.budgetAmount / 100).toFixed(2)}` : null;
        return [scopeName, budgetAmount].filter(Boolean).join(" · ");
    }
    return Object.entries(payload)
        .slice(0, 2)
        .map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`)
        .join(" · ");
}
function approvalRequesterLabel(approval, currentUserId) {
    if (approval.requestedByUserId && approval.requestedByUserId === currentUserId)
        return "You";
    if (approval.requestedByUserId)
        return "User";
    if (approval.requestedByAgentId)
        return "Agent";
    return "System";
}
function approvalActions(approval) {
    return [
        buildAction("Approve", `/approvals/${approval.id}/approve`, "POST"),
        buildAction("Reject", `/approvals/${approval.id}/reject`, "POST"),
        buildAction("Request changes", `/approvals/${approval.id}/request-revision`, "POST"),
        buildAction("Expand details", `/messenger/approvals/${approval.id}`, "GET"),
        buildAction("Open full approval", `/messenger/approvals/${approval.id}`, "GET"),
    ];
}
function issueActions(issue, currentUserId) {
    const actions = [
        buildAction("Open issue", issueHref(issue), "GET"),
        buildAction("Quick comment", `${issueHref(issue)}/comments`, "POST"),
    ];
    return actions;
}
function chatSummary(conversation) {
    const preview = conversation.latestReplyPreview ?? truncate(conversation.summary, 140) ?? truncate(conversation.title, 140) ?? "Start the conversation";
    return {
        threadKey: threadKeyForChat(conversation.id),
        kind: "chat",
        title: formatMessengerTitle(conversation.title, { max: 80 }) ?? conversation.title,
        subtitle: preview,
        preview,
        latestActivityAt: conversation.lastMessageAt ?? conversation.updatedAt,
        lastReadAt: conversation.lastReadAt,
        unreadCount: conversation.unreadCount,
        needsAttention: conversation.needsAttention,
        isPinned: conversation.isPinned,
        href: `/messenger/chat/${conversation.id}`,
    };
}
function issueSummary(issueCount, latestActivityAt, unreadCount, lastReadAt, preview) {
    return {
        threadKey: "issues",
        kind: "issues",
        title: "Issues",
        subtitle: issueCount > 0 ? `${issueCount} tracked issue${issueCount === 1 ? "" : "s"}` : "No tracked issues yet",
        preview: issueCount > 0 ? preview ?? "Cross-issue activity feed" : "Create or follow issues to populate this feed",
        latestActivityAt,
        lastReadAt,
        unreadCount,
        needsAttention: unreadCount > 0,
        isPinned: false,
        href: "/messenger/issues",
    };
}
function approvalSummary(approvalCount, latestActivityAt, unreadCount, lastReadAt, preview) {
    return {
        threadKey: "approvals",
        kind: "approvals",
        title: "Approvals",
        subtitle: approvalCount > 0
            ? `${approvalCount} approval${approvalCount === 1 ? "" : "s"}`
            : "No approvals yet",
        preview: approvalCount > 0 ? preview ?? "Review and decide on pending approvals" : "No approvals in this organization",
        latestActivityAt,
        lastReadAt,
        unreadCount,
        needsAttention: unreadCount > 0,
        isPinned: false,
        href: "/messenger/approvals",
    };
}
function systemSummary(kind, title, itemCount, latestActivityAt, unreadCount, lastReadAt, subtitleWhenEmpty, preview) {
    return {
        threadKey: kind,
        kind,
        title,
        subtitle: itemCount > 0 ? `${itemCount} item${itemCount === 1 ? "" : "s"}` : subtitleWhenEmpty,
        preview: itemCount > 0 ? preview ?? "Aggregate operational updates" : subtitleWhenEmpty,
        latestActivityAt,
        lastReadAt,
        unreadCount,
        needsAttention: unreadCount > 0,
        isPinned: false,
        href: `/messenger/system/${kind}`,
    };
}
function issueCard(issue, currentUserId, followed, latestPreview, latestActivityAt, sourceComment, latestActivity) {
    const createdByMe = issue.createdByUserId === currentUserId;
    const assignedToMe = issue.assigneeUserId === currentUserId;
    const reviewerForMe = issue.reviewerUserId === currentUserId && issue.status === "in_review";
    const statusChange = issueStatusChangeFromActivity(latestActivity);
    const sourceCommentAuthorKind = sourceComment?.authorAgentId
        ? "agent"
        : sourceComment?.authorUserId ? "user" : "system";
    const sourceCommentByMe = Boolean(sourceComment?.authorUserId && sourceComment.authorUserId === currentUserId);
    const sourceCommentAuthorLabel = issueCommentAuthorLabel(sourceComment, currentUserId);
    return {
        id: issue.id,
        threadKey: "issues",
        kind: "issues",
        title: issueDisplayLabel(issue),
        subtitle: issueBodyFromSnapshot(issue, latestPreview, followed, createdByMe, assignedToMe, reviewerForMe),
        body: issueBodyFromSnapshot(issue, latestPreview, followed, createdByMe, assignedToMe, reviewerForMe),
        preview: latestPreview,
        href: issueHref(issue),
        latestActivityAt,
        actions: issueActions(issue, currentUserId),
        metadata: {
            issueId: issue.id,
            issueIdentifier: issue.identifier,
            status: issue.status,
            ...(statusChange ? { statusChange } : {}),
            priority: issue.priority,
            followed,
            createdByMe,
            assignedToMe,
            reviewerForMe,
            ...(sourceComment
                ? {
                    sourceCommentAuthorKind,
                    sourceCommentByMe,
                    sourceCommentAuthorLabel,
                }
                : {}),
        },
        issueId: issue.id,
        issueIdentifier: issue.identifier,
        sourceCommentId: sourceComment?.id ?? null,
        sourceCommentAuthorLabel,
        sourceCommentBody: sourceComment?.body ?? null,
    };
}
function approvalCard(approval, latestComment, currentUserId, latestActivityAt) {
    const payloadPreview = summarizeApprovalPayload(approval);
    const body = latestComment ? truncate(latestComment.body) : approval.decisionNote ?? payloadPreview;
    const title = approval.type === "chat_issue_creation"
        ? "Review proposed issue"
        : approval.type === "chat_operation"
            ? "Review chat operation"
            : approval.type.replaceAll("_", " ");
    return {
        id: approval.id,
        threadKey: "approvals",
        kind: "approvals",
        title,
        subtitle: `${approvalRequesterLabel(approval, currentUserId)} · ${approval.status.replaceAll("_", " ")}`,
        body,
        preview: body,
        href: `/messenger/approvals/${approval.id}`,
        latestActivityAt,
        actions: approvalActions(approval),
        metadata: {
            approvalId: approval.id,
            type: approval.type,
            status: approval.status,
            payload: redactEventPayload(approval.payload),
            requester: approvalRequesterLabel(approval, currentUserId),
        },
        approval: approval,
    };
}
function failedRunCard(run, agentName) {
    return {
        id: run.id,
        threadKey: "failed-runs",
        kind: "failed-runs",
        title: agentName ? `${agentName} · Failed run` : "Failed run",
        subtitle: run.status.replaceAll("_", " "),
        body: truncate(run.error) ?? truncate(run.stderrExcerpt) ?? "Run exited with an error.",
        preview: truncate(run.error) ?? truncate(run.stderrExcerpt),
        href: `/agents/${run.agentId}/runs/${run.id}`,
        latestActivityAt: run.updatedAt ?? run.createdAt,
        actions: [
            buildAction("Retry", `/heartbeat-runs/${run.id}/retry`, "POST"),
            buildAction("Open run", `/agents/${run.agentId}/runs/${run.id}`, "GET"),
        ],
        metadata: {
            runId: run.id,
            agentId: run.agentId,
            status: run.status,
            contextSnapshot: run.contextSnapshot,
        },
        run: run,
    };
}
function budgetCard(incident) {
    return {
        id: incident.id,
        threadKey: "budget-alerts",
        kind: "budget-alerts",
        title: incident.scopeName || "Budget alert",
        subtitle: `${incident.scopeType} · ${incident.thresholdType}`,
        body: `${incident.metric.replaceAll("_", " ")} ${incident.amountObserved} / ${incident.amountLimit}`,
        preview: `${incident.amountObserved} observed against ${incident.amountLimit} limit`,
        href: "/costs",
        latestActivityAt: incident.updatedAt ?? incident.createdAt,
        actions: [buildAction("Open budget", "/costs", "GET")],
        metadata: {
            incidentId: incident.id,
            scopeType: incident.scopeType,
            scopeId: incident.scopeId,
            status: incident.status,
            thresholdType: incident.thresholdType,
        },
        incident: incident,
    };
}
function joinRequestCard(request) {
    const title = request.agentName ?? request.requestEmailSnapshot ?? request.requestType.replaceAll("_", " ");
    return {
        id: request.id,
        threadKey: "join-requests",
        kind: "join-requests",
        title,
        subtitle: `${request.status.replaceAll("_", " ")} · ${request.requestType.replaceAll("_", " ")}`,
        body: (request.capabilities ?? request.agentDefaultsPayload)
            ? "Join request needs approval"
            : "Join request",
        preview: request.capabilities ?? request.requestEmailSnapshot ?? null,
        href: null,
        latestActivityAt: request.updatedAt ?? request.createdAt,
        actions: [
            buildAction("Approve", `/orgs/${request.orgId}/join-requests/${request.id}/approve`, "POST"),
            buildAction("Reject", `/orgs/${request.orgId}/join-requests/${request.id}/reject`, "POST"),
        ],
        metadata: {
            requestId: request.id,
            orgId: request.orgId,
            requestType: request.requestType,
            status: request.status,
        },
        joinRequest: request,
    };
}
function systemUnreadCountSince(rows, lastReadAt) {
    if (!lastReadAt)
        return rows.length;
    return rows.filter((row) => {
        const activityAt = normalizeDate(row.updatedAt ?? row.createdAt ?? null);
        return Boolean(activityAt && activityAt.getTime() > lastReadAt.getTime());
    }).length;
}
async function loadThreadStates(db, orgId, userId, threadKeys) {
    if (threadKeys.length === 0)
        return new Map();
    const rows = await db
        .select()
        .from(messengerThreadUserStates)
        .where(and(eq(messengerThreadUserStates.orgId, orgId), eq(messengerThreadUserStates.userId, userId), inArray(messengerThreadUserStates.threadKey, threadKeys)));
    return new Map(rows.map((row) => [row.threadKey, row]));
}
export function messengerService(db) {
    const issuesSvc = issueService(db);
    const chatsSvc = chatService(db);
    const budgetsSvc = budgetService(db);
    async function loadIssueUniverse(orgId, userId) {
        const [followRows, trackedRows] = await Promise.all([
            issuesSvc.listFollows(orgId, userId),
            db
                .select({
                id: issues.id,
                orgId: issues.orgId,
                title: issues.title,
                status: issues.status,
                priority: issues.priority,
                assigneeUserId: issues.assigneeUserId,
                reviewerUserId: issues.reviewerUserId,
                createdByUserId: issues.createdByUserId,
                identifier: issues.identifier,
                updatedAt: issues.updatedAt,
            })
                .from(issues)
                .where(and(eq(issues.orgId, orgId), or(eq(issues.assigneeUserId, userId), eq(issues.createdByUserId, userId), eq(issues.reviewerUserId, userId)), isNull(issues.hiddenAt)))
                .orderBy(desc(issues.updatedAt)),
        ]);
        const universe = new Map();
        for (const row of followRows) {
            universe.set(row.issueId, {
                ...row.issue,
                followed: true,
                assigned: row.issue.assigneeUserId === userId,
            });
        }
        for (const row of trackedRows) {
            const existing = universe.get(row.id);
            universe.set(row.id, {
                ...row,
                followed: existing?.followed ?? false,
                assigned: row.assigneeUserId === userId,
            });
        }
        return Array.from(universe.values());
    }
    async function loadIssueSummaryData(orgId, userId) {
        const issuesUniverse = await loadIssueUniverse(orgId, userId);
        const issueIds = issuesUniverse.map((row) => row.id);
        const threadStates = await loadThreadStates(db, orgId, userId, ["issues"]);
        const lastReadAt = threadStates.get("issues")?.lastReadAt ?? null;
        const [commentRows, activityRows] = await Promise.all([
            issueIds.length === 0
                ? Promise.resolve([])
                : db
                    .select({
                    id: issueComments.id,
                    issueId: issueComments.issueId,
                    body: issueComments.body,
                    authorAgentId: issueComments.authorAgentId,
                    authorUserId: issueComments.authorUserId,
                    authorAgentName: agents.name,
                    authorUserName: authUsers.name,
                    createdAt: issueComments.createdAt,
                })
                    .from(issueComments)
                    .leftJoin(agents, eq(issueComments.authorAgentId, agents.id))
                    .leftJoin(authUsers, eq(issueComments.authorUserId, authUsers.id))
                    .where(and(eq(issueComments.orgId, orgId), inArray(issueComments.issueId, issueIds)))
                    .orderBy(desc(issueComments.createdAt)),
            issueIds.length === 0
                ? Promise.resolve([])
                : db
                    .select({
                    id: activityLog.id,
                    action: activityLog.action,
                    entityId: activityLog.entityId,
                    actorType: activityLog.actorType,
                    actorId: activityLog.actorId,
                    details: activityLog.details,
                    createdAt: activityLog.createdAt,
                    runId: activityLog.runId,
                })
                    .from(activityLog)
                    .where(and(eq(activityLog.orgId, orgId), eq(activityLog.entityType, "issue"), inArray(activityLog.entityId, issueIds), inArray(activityLog.action, ISSUE_ACTIVITY_ACTIONS)))
                    .orderBy(desc(activityLog.createdAt)),
        ]);
        const latestCommentByIssue = new Map();
        const latestExternalCommentByIssue = new Map();
        for (const row of commentRows) {
            if (!latestCommentByIssue.has(row.issueId)) {
                latestCommentByIssue.set(row.issueId, row);
            }
            if (!isSelfAuthoredComment(row, userId) && !latestExternalCommentByIssue.has(row.issueId)) {
                latestExternalCommentByIssue.set(row.issueId, row);
            }
        }
        const latestActivityByIssue = new Map();
        const latestExternalActivityByIssue = new Map();
        const latestSuppressedActivityByIssue = new Map();
        for (const row of activityRows) {
            if (!shouldNotifyIssueActivity(row)) {
                if (!latestSuppressedActivityByIssue.has(row.entityId)) {
                    latestSuppressedActivityByIssue.set(row.entityId, row);
                }
                continue;
            }
            if (!latestActivityByIssue.has(row.entityId)) {
                latestActivityByIssue.set(row.entityId, row);
            }
            if (!isSelfAuthoredActivity(row, userId) && !latestExternalActivityByIssue.has(row.entityId)) {
                latestExternalActivityByIssue.set(row.entityId, row);
            }
        }
        const unsortedEntries = issuesUniverse.map((issue) => {
            const latestComment = latestCommentByIssue.get(issue.id) ?? null;
            const latestExternalComment = latestExternalCommentByIssue.get(issue.id) ?? null;
            const latestActivity = latestActivityByIssue.get(issue.id) ?? null;
            const latestVisibleComment = latestExternalComment;
            const latestVisibleCommentAt = normalizeDate(latestVisibleComment?.createdAt ?? null);
            const latestEventAt = maxDate(latestVisibleCommentAt, latestActivity?.createdAt);
            const latestActivityAt = maxDate(issue.updatedAt, latestEventAt);
            const latestSourceIsComment = latestVisibleCommentAt &&
                (!latestActivity?.createdAt || latestVisibleCommentAt.getTime() >= new Date(latestActivity.createdAt).getTime());
            const latestPreview = latestSourceIsComment
                ? truncate(latestVisibleComment?.body)
                : latestActivity
                    ? summarizeIssueActivity(latestActivity, issue)
                    : null;
            const statusChangeActivity = latestSourceIsComment
                ? (issueStatusActivityMatchesSourceComment(latestActivity, latestVisibleComment) ? latestActivity : null)
                : latestActivity;
            const latestExternalActivity = latestExternalActivityByIssue.get(issue.id) ?? null;
            const latestExternalCommentAt = normalizeDate(latestExternalComment?.createdAt ?? null);
            const latestSuppressedActivityAt = normalizeDate(latestSuppressedActivityByIssue.get(issue.id)?.createdAt ?? null);
            const issueUpdatedAt = normalizeDate(issue.updatedAt);
            const suppressedActivityMatchesIssueUpdate = Boolean(latestSuppressedActivityAt &&
                issueUpdatedAt &&
                latestSuppressedActivityAt.getTime() >= issueUpdatedAt.getTime() - 5_000);
            const fallbackAssignedActivityAt = issue.assigneeUserId === userId && !latestActivityByIssue.has(issue.id) && !suppressedActivityMatchesIssueUpdate
                ? issueUpdatedAt
                : null;
            const fallbackReviewerActivityAt = issue.reviewerUserId === userId && issue.status === "in_review" && !latestActivityByIssue.has(issue.id) && !suppressedActivityMatchesIssueUpdate
                ? issueUpdatedAt
                : null;
            const attentionActivityAt = maxDate(latestExternalCommentAt, latestExternalActivity?.createdAt, fallbackAssignedActivityAt, fallbackReviewerActivityAt);
            const attentionPreview = latestExternalCommentAt &&
                (!latestExternalActivity?.createdAt || latestExternalCommentAt.getTime() >= new Date(latestExternalActivity.createdAt).getTime())
                ? truncate(latestExternalComment?.body)
                : latestExternalActivity
                    ? summarizeIssueActivity(latestExternalActivity, issue)
                    : fallbackAssignedActivityAt || fallbackReviewerActivityAt
                        ? issueBodyFromSnapshot(issue, null, issue.followed, issue.createdByUserId === userId, issue.assigneeUserId === userId, issue.reviewerUserId === userId && issue.status === "in_review")
                        : null;
            const summaryPreview = attentionActivityAt ? issueThreadPreview(issue, attentionPreview) : null;
            return {
                item: issueCard(issue, userId, issue.followed, latestPreview, latestActivityAt ?? issue.updatedAt, latestSourceIsComment ? latestVisibleComment : null, statusChangeActivity),
                attentionActivityAt,
                attentionPreview: summaryPreview,
            };
        });
        const unsortedItems = unsortedEntries.map((entry) => entry.item);
        const latestFirstEntries = [...unsortedEntries].sort((a, b) => {
            const aTime = a.attentionActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
            const bTime = b.attentionActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
            if (aTime !== bTime)
                return bTime - aTime;
            return a.item.title.localeCompare(b.item.title);
        });
        const chronologicalItems = [...unsortedItems].sort(compareChronologicalActivity);
        const latestAttentionEntry = latestFirstEntries.find((entry) => entry.attentionActivityAt);
        const latestActivityAt = latestAttentionEntry?.attentionActivityAt ?? null;
        const unreadCount = unsortedEntries.filter((entry) => {
            const itemActivity = entry.attentionActivityAt;
            if (!itemActivity)
                return false;
            if (!lastReadAt)
                return true;
            return itemActivity.getTime() > lastReadAt.getTime();
        }).length;
        return {
            summary: issueSummary(issuesUniverse.length, latestActivityAt, unreadCount, lastReadAt, latestAttentionEntry?.attentionPreview ?? null),
            detail: {
                threadKey: "issues",
                kind: "issues",
                title: "Issues",
                subtitle: `${issuesUniverse.length} tracked issue${issuesUniverse.length === 1 ? "" : "s"}`,
                preview: latestAttentionEntry?.attentionPreview ?? null,
                latestActivityAt,
                lastReadAt,
                unreadCount,
                needsAttention: unreadCount > 0,
                isPinned: false,
                href: "/messenger/issues",
                description: "Followed issues, issues I created, issues assigned to me, and issues ready for my review",
                items: chronologicalItems,
            },
        };
    }
    async function loadApprovalSummaryData(orgId, userId) {
        const threadStates = await loadThreadStates(db, orgId, userId, ["approvals"]);
        const lastReadAt = threadStates.get("approvals")?.lastReadAt ?? null;
        const [approvalRows, latestComments] = await Promise.all([
            db
                .select()
                .from(approvals)
                .where(eq(approvals.orgId, orgId))
                .orderBy(desc(approvals.updatedAt), desc(approvals.createdAt)),
            db
                .select({
                approvalId: approvalComments.approvalId,
                body: approvalComments.body,
                createdAt: approvalComments.createdAt,
            })
                .from(approvalComments)
                .innerJoin(approvals, eq(approvalComments.approvalId, approvals.id))
                .where(eq(approvals.orgId, orgId))
                .orderBy(desc(approvalComments.createdAt)),
        ]);
        const latestCommentByApproval = new Map();
        for (const row of latestComments) {
            if (!latestCommentByApproval.has(row.approvalId)) {
                latestCommentByApproval.set(row.approvalId, row);
            }
        }
        const typedApprovalRows = approvalRows;
        const unsortedItems = typedApprovalRows.map((approval) => {
            const latestComment = latestCommentByApproval.get(approval.id) ?? null;
            const latestActivityAt = maxDate(approval.updatedAt, latestComment?.createdAt) ?? approval.updatedAt;
            return approvalCard(approval, latestComment, userId, latestActivityAt);
        });
        const latestFirstItems = [...unsortedItems].sort(compareLatestActivity);
        const chronologicalItems = [...unsortedItems].sort(compareChronologicalActivity);
        const actionable = typedApprovalRows.filter((approval) => ACTIONABLE_APPROVAL_STATUSES.has(approval.status));
        const unreadCount = actionable.filter((approval) => {
            const activityAt = normalizeDate(approval.updatedAt);
            if (!activityAt)
                return false;
            if (!lastReadAt)
                return true;
            return activityAt.getTime() > lastReadAt.getTime();
        }).length;
        const latestActivityAt = latestFirstItems[0]?.latestActivityAt ?? null;
        return {
            summary: approvalSummary(approvalRows.length, latestActivityAt, unreadCount, lastReadAt, latestFirstItems[0]?.preview ?? null),
            detail: {
                threadKey: "approvals",
                kind: "approvals",
                title: "Approvals",
                subtitle: `${approvalRows.length} approval${approvalRows.length === 1 ? "" : "s"}`,
                preview: latestFirstItems[0]?.preview ?? null,
                latestActivityAt,
                lastReadAt,
                unreadCount,
                needsAttention: unreadCount > 0,
                isPinned: false,
                href: "/messenger/approvals",
                description: "Approvals needing attention",
                items: chronologicalItems,
            },
        };
    }
    async function loadFailedRunData(orgId, userId) {
        const threadStates = await loadThreadStates(db, orgId, userId, ["failed-runs"]);
        const lastReadAt = threadStates.get("failed-runs")?.lastReadAt ?? null;
        const [runRows, agentRows] = await Promise.all([
            db
                .select({
                id: heartbeatRuns.id,
                orgId: heartbeatRuns.orgId,
                agentId: heartbeatRuns.agentId,
                status: heartbeatRuns.status,
                error: heartbeatRuns.error,
                stderrExcerpt: heartbeatRuns.stderrExcerpt,
                stdoutExcerpt: heartbeatRuns.stdoutExcerpt,
                contextSnapshot: heartbeatRuns.contextSnapshot,
                createdAt: heartbeatRuns.createdAt,
                updatedAt: heartbeatRuns.updatedAt,
            })
                .from(heartbeatRuns)
                .where(and(eq(heartbeatRuns.orgId, orgId), eq(heartbeatRuns.status, "failed")))
                .orderBy(desc(heartbeatRuns.updatedAt), desc(heartbeatRuns.createdAt)),
            db
                .select({
                id: agents.id,
                name: agents.name,
            })
                .from(agents)
                .where(eq(agents.orgId, orgId)),
        ]);
        const agentNames = new Map(agentRows.map((row) => [row.id, row.name]));
        const items = runRows.map((run) => failedRunCard(run, agentNames.get(run.agentId) ?? null));
        const latestFirstItems = [...items].sort(compareLatestActivity);
        const chronologicalItems = [...items].sort(compareChronologicalActivity);
        const latestActivityAt = latestFirstItems[0]?.latestActivityAt ?? null;
        const unreadCount = systemUnreadCountSince(runRows, lastReadAt);
        return {
            summary: systemSummary("failed-runs", "Failed runs", runRows.length, latestActivityAt, unreadCount, lastReadAt, "No failed runs yet", latestFirstItems[0]?.preview ?? null),
            detail: {
                threadKey: "failed-runs",
                kind: "failed-runs",
                title: "Failed runs",
                subtitle: `${runRows.length} recent failure${runRows.length === 1 ? "" : "s"}`,
                preview: latestFirstItems[0]?.preview ?? null,
                latestActivityAt,
                lastReadAt,
                unreadCount,
                needsAttention: unreadCount > 0,
                isPinned: false,
                href: "/messenger/system/failed-runs",
                description: "Recent failed heartbeat runs",
                items: chronologicalItems,
            },
        };
    }
    async function loadBudgetAlertData(orgId, userId) {
        const threadStates = await loadThreadStates(db, orgId, userId, ["budget-alerts"]);
        const lastReadAt = threadStates.get("budget-alerts")?.lastReadAt ?? null;
        const incidents = ((await budgetsSvc.overview(orgId)).activeIncidents ?? []);
        const items = incidents.map((incident) => budgetCard(incident));
        const latestActivityAt = items[0]?.latestActivityAt ?? null;
        const unreadCount = systemUnreadCountSince(incidents, lastReadAt);
        return {
            summary: systemSummary("budget-alerts", "Budget alerts", incidents.length, latestActivityAt, unreadCount, lastReadAt, "No budget alerts yet", items[0]?.preview ?? null),
            detail: {
                threadKey: "budget-alerts",
                kind: "budget-alerts",
                title: "Budget alerts",
                subtitle: `${incidents.length} active alert${incidents.length === 1 ? "" : "s"}`,
                preview: items[0]?.preview ?? null,
                latestActivityAt,
                lastReadAt,
                unreadCount,
                needsAttention: unreadCount > 0,
                isPinned: false,
                href: "/messenger/system/budget-alerts",
                description: "Open budget incidents",
                items,
            },
        };
    }
    async function loadJoinRequestData(orgId, userId) {
        const threadStates = await loadThreadStates(db, orgId, userId, ["join-requests"]);
        const lastReadAt = threadStates.get("join-requests")?.lastReadAt ?? null;
        const rows = (await db
            .select()
            .from(joinRequests)
            .where(and(eq(joinRequests.orgId, orgId), eq(joinRequests.status, "pending_approval")))
            .orderBy(desc(joinRequests.updatedAt), desc(joinRequests.createdAt)));
        const items = rows.map((row) => joinRequestCard(row));
        const latestActivityAt = items[0]?.latestActivityAt ?? null;
        const unreadCount = systemUnreadCountSince(rows, lastReadAt);
        return {
            summary: systemSummary("join-requests", "Join requests", rows.length, latestActivityAt, unreadCount, lastReadAt, "No pending join requests", items[0]?.preview ?? null),
            detail: {
                threadKey: "join-requests",
                kind: "join-requests",
                title: "Join requests",
                subtitle: `${rows.length} pending request${rows.length === 1 ? "" : "s"}`,
                preview: items[0]?.preview ?? null,
                latestActivityAt,
                lastReadAt,
                unreadCount,
                needsAttention: unreadCount > 0,
                isPinned: false,
                href: "/messenger/system/join-requests",
                description: "Pending organization join requests",
                items,
            },
        };
    }
    async function listThreadSummaries(orgId, userId) {
        const [chats, issueData, approvalData, failedRunData, budgetData, joinRequestData] = await Promise.all([
            chatsSvc.list(orgId, { status: "active" }, userId),
            loadIssueSummaryData(orgId, userId),
            loadApprovalSummaryData(orgId, userId),
            loadFailedRunData(orgId, userId),
            loadBudgetAlertData(orgId, userId),
            loadJoinRequestData(orgId, userId),
        ]);
        const syntheticSummaries = [];
        if (issueData.detail.items.length > 0)
            syntheticSummaries.push(issueData.summary);
        if (approvalData.detail.items.length > 0)
            syntheticSummaries.push(approvalData.summary);
        if (failedRunData.detail.items.length > 0)
            syntheticSummaries.push(failedRunData.summary);
        if (budgetData.detail.items.length > 0)
            syntheticSummaries.push(budgetData.summary);
        if (joinRequestData.detail.items.length > 0)
            syntheticSummaries.push(joinRequestData.summary);
        const threadSummaries = [
            ...chats.map(chatSummary),
            ...syntheticSummaries,
        ].sort((a, b) => {
            const aTime = a.latestActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
            const bTime = b.latestActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
            if (aTime !== bTime)
                return bTime - aTime;
            return a.title.localeCompare(b.title);
        });
        return threadSummaries;
    }
    async function getIssuesThread(orgId, userId) {
        return loadIssueSummaryData(orgId, userId);
    }
    async function getApprovalsThread(orgId, userId) {
        return loadApprovalSummaryData(orgId, userId);
    }
    async function getSystemThread(orgId, userId, threadKind) {
        switch (threadKind) {
            case "failed-runs":
                return loadFailedRunData(orgId, userId);
            case "budget-alerts":
                return loadBudgetAlertData(orgId, userId);
            case "join-requests":
                return loadJoinRequestData(orgId, userId);
            default:
                return null;
        }
    }
    async function getChatThread(conversationId, userId) {
        const conversation = await chatsSvc.getById(conversationId, userId);
        if (!conversation)
            return null;
        const messages = await chatsSvc.listMessages(conversationId);
        return {
            conversation: conversation,
            messages: messages,
        };
    }
    async function getThreadState(orgId, userId, threadKey) {
        return db
            .select()
            .from(messengerThreadUserStates)
            .where(and(eq(messengerThreadUserStates.orgId, orgId), eq(messengerThreadUserStates.userId, userId), eq(messengerThreadUserStates.threadKey, threadKey)))
            .then((rows) => rows[0] ?? null);
    }
    async function markThreadRead(orgId, userId, threadKey, readAt = new Date()) {
        if (threadKey.startsWith("chat:")) {
            const conversationId = threadKey.slice("chat:".length);
            const conversation = await chatsSvc.getById(conversationId, userId);
            if (!conversation || conversation.orgId !== orgId) {
                return null;
            }
            const state = await chatsSvc.markRead(conversationId, orgId, userId, readAt);
            if (!state)
                return null;
            return { lastReadAt: state.lastReadAt };
        }
        const now = new Date();
        const [row] = await db
            .insert(messengerThreadUserStates)
            .values({
            orgId,
            userId,
            threadKey,
            lastReadAt: readAt,
            updatedAt: now,
        })
            .onConflictDoUpdate({
            target: [
                messengerThreadUserStates.orgId,
                messengerThreadUserStates.threadKey,
                messengerThreadUserStates.userId,
            ],
            set: {
                lastReadAt: readAt,
                updatedAt: now,
            },
        })
            .returning();
        return row ? { lastReadAt: row.lastReadAt } : null;
    }
    return {
        listThreadSummaries,
        getChatThread,
        getIssuesThread,
        getApprovalsThread,
        getSystemThread,
        getThreadState,
        markThreadRead,
        setThreadRead: markThreadRead,
    };
}
//# sourceMappingURL=messenger.js.map