import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
import { agents, assets, issueAttachments, issueComments, issues, projects, } from "@rudderhq/db";
import { extractAgentMentionIds, extractProjectMentionIds, isUuidLike, } from "@rudderhq/shared";
import { notFound, unprocessable } from "../errors.js";
import { redactCurrentUserText } from "../log-redaction.js";
import { MAX_ISSUE_COMMENT_PAGE_LIMIT } from "./issues.helpers.js";
export function createIssueCommentAttachmentMethods(ctx) {
    const { db, instanceSettings, redactIssueComment } = ctx;
    return {
        findMentionedAgents: async (orgId, body) => {
            const re = /\B@([^\s@,!?.]+)/g;
            const tokens = new Set();
            let m;
            while ((m = re.exec(body)) !== null)
                tokens.add(m[1].toLowerCase());
            const explicitAgentMentionIds = extractAgentMentionIds(body).filter(isUuidLike);
            if (tokens.size === 0 && explicitAgentMentionIds.length === 0)
                return [];
            const rows = await db.select({ id: agents.id, name: agents.name })
                .from(agents).where(eq(agents.orgId, orgId));
            const resolved = new Set(explicitAgentMentionIds);
            for (const agent of rows) {
                if (tokens.has(agent.name.toLowerCase())) {
                    resolved.add(agent.id);
                }
            }
            return [...resolved];
        },
        findMentionedProjectIds: async (issueId) => {
            const issue = await db
                .select({
                orgId: issues.orgId,
                title: issues.title,
                description: issues.description,
            })
                .from(issues)
                .where(eq(issues.id, issueId))
                .then((rows) => rows[0] ?? null);
            if (!issue)
                return [];
            const comments = await db
                .select({ body: issueComments.body })
                .from(issueComments)
                .where(eq(issueComments.issueId, issueId));
            const mentionedIds = new Set();
            for (const source of [
                issue.title,
                issue.description ?? "",
                ...comments.map((comment) => comment.body),
            ]) {
                for (const projectId of extractProjectMentionIds(source)) {
                    mentionedIds.add(projectId);
                }
            }
            if (mentionedIds.size === 0)
                return [];
            const validMentionedIds = [...mentionedIds].filter(isUuidLike);
            if (validMentionedIds.length === 0)
                return [];
            const rows = await db
                .select({ id: projects.id })
                .from(projects)
                .where(and(eq(projects.orgId, issue.orgId), inArray(projects.id, validMentionedIds)));
            const valid = new Set(rows.map((row) => row.id));
            return validMentionedIds.filter((projectId) => valid.has(projectId));
        },
        listComments: async (issueId, opts) => {
            const order = opts?.order === "asc" ? "asc" : "desc";
            const afterCommentId = opts?.afterCommentId?.trim() || null;
            const limit = opts?.limit && opts.limit > 0
                ? Math.min(Math.floor(opts.limit), MAX_ISSUE_COMMENT_PAGE_LIMIT)
                : null;
            const conditions = [eq(issueComments.issueId, issueId)];
            if (afterCommentId) {
                const anchor = await db
                    .select({
                    id: issueComments.id,
                    createdAt: issueComments.createdAt,
                })
                    .from(issueComments)
                    .where(and(eq(issueComments.issueId, issueId), eq(issueComments.id, afterCommentId)))
                    .then((rows) => rows[0] ?? null);
                if (!anchor)
                    return [];
                const anchorCreatedAt = anchor.createdAt instanceof Date
                    ? anchor.createdAt.toISOString()
                    : new Date(anchor.createdAt).toISOString();
                conditions.push(order === "asc"
                    ? sql `(
                ${issueComments.createdAt} > ${anchorCreatedAt}
                OR (${issueComments.createdAt} = ${anchorCreatedAt} AND ${issueComments.id} > ${anchor.id})
              )`
                    : sql `(
                ${issueComments.createdAt} < ${anchorCreatedAt}
                OR (${issueComments.createdAt} = ${anchorCreatedAt} AND ${issueComments.id} < ${anchor.id})
              )`);
            }
            const query = db
                .select()
                .from(issueComments)
                .where(and(...conditions))
                .orderBy(order === "asc" ? asc(issueComments.createdAt) : desc(issueComments.createdAt), order === "asc" ? asc(issueComments.id) : desc(issueComments.id));
            const comments = limit ? await query.limit(limit) : await query;
            const { censorUsernameInLogs } = await instanceSettings.getGeneral();
            return comments.map((comment) => redactIssueComment(comment, censorUsernameInLogs));
        },
        getCommentCursor: async (issueId) => {
            const [latest, countRow] = await Promise.all([
                db
                    .select({
                    latestCommentId: issueComments.id,
                    latestCommentAt: issueComments.createdAt,
                })
                    .from(issueComments)
                    .where(eq(issueComments.issueId, issueId))
                    .orderBy(desc(issueComments.createdAt), desc(issueComments.id))
                    .limit(1)
                    .then((rows) => rows[0] ?? null),
                db
                    .select({
                    totalComments: sql `count(*)::int`,
                })
                    .from(issueComments)
                    .where(eq(issueComments.issueId, issueId))
                    .then((rows) => rows[0] ?? null),
            ]);
            return {
                totalComments: Number(countRow?.totalComments ?? 0),
                latestCommentId: latest?.latestCommentId ?? null,
                latestCommentAt: latest?.latestCommentAt ?? null,
            };
        },
        getComment: (commentId) => instanceSettings.getGeneral().then(({ censorUsernameInLogs }) => db
            .select()
            .from(issueComments)
            .where(eq(issueComments.id, commentId))
            .then((rows) => {
            const comment = rows[0] ?? null;
            return comment ? redactIssueComment(comment, censorUsernameInLogs) : null;
        })),
        addComment: async (issueId, body, actor) => {
            const issue = await db
                .select({ orgId: issues.orgId })
                .from(issues)
                .where(eq(issues.id, issueId))
                .then((rows) => rows[0] ?? null);
            if (!issue)
                throw notFound("Issue not found");
            const currentUserRedactionOptions = {
                enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs,
            };
            const redactedBody = redactCurrentUserText(body, currentUserRedactionOptions);
            const [comment] = await db
                .insert(issueComments)
                .values({
                orgId: issue.orgId,
                issueId,
                authorAgentId: actor.agentId ?? null,
                authorUserId: actor.userId ?? null,
                body: redactedBody,
            })
                .returning();
            // Update issue's updatedAt so comment activity is reflected in recency sorting
            await db
                .update(issues)
                .set({ updatedAt: new Date() })
                .where(eq(issues.id, issueId));
            return redactIssueComment(comment, currentUserRedactionOptions.enabled);
        },
        createAttachment: async (input) => {
            const issue = await db
                .select({ id: issues.id, orgId: issues.orgId })
                .from(issues)
                .where(eq(issues.id, input.issueId))
                .then((rows) => rows[0] ?? null);
            if (!issue)
                throw notFound("Issue not found");
            if (input.issueCommentId) {
                const comment = await db
                    .select({ id: issueComments.id, orgId: issueComments.orgId, issueId: issueComments.issueId })
                    .from(issueComments)
                    .where(eq(issueComments.id, input.issueCommentId))
                    .then((rows) => rows[0] ?? null);
                if (!comment)
                    throw notFound("Issue comment not found");
                if (comment.orgId !== issue.orgId || comment.issueId !== issue.id) {
                    throw unprocessable("Attachment comment must belong to same issue and organization");
                }
            }
            return db.transaction(async (tx) => {
                const [asset] = await tx
                    .insert(assets)
                    .values({
                    orgId: issue.orgId,
                    provider: input.provider,
                    objectKey: input.objectKey,
                    contentType: input.contentType,
                    byteSize: input.byteSize,
                    sha256: input.sha256,
                    originalFilename: input.originalFilename ?? null,
                    createdByAgentId: input.createdByAgentId ?? null,
                    createdByUserId: input.createdByUserId ?? null,
                })
                    .returning();
                const [attachment] = await tx
                    .insert(issueAttachments)
                    .values({
                    orgId: issue.orgId,
                    issueId: issue.id,
                    assetId: asset.id,
                    issueCommentId: input.issueCommentId ?? null,
                    usage: input.usage ?? "issue",
                })
                    .returning();
                return {
                    id: attachment.id,
                    orgId: attachment.orgId,
                    issueId: attachment.issueId,
                    issueCommentId: attachment.issueCommentId,
                    assetId: attachment.assetId,
                    usage: attachment.usage,
                    provider: asset.provider,
                    objectKey: asset.objectKey,
                    contentType: asset.contentType,
                    byteSize: asset.byteSize,
                    sha256: asset.sha256,
                    originalFilename: asset.originalFilename,
                    createdByAgentId: asset.createdByAgentId,
                    createdByUserId: asset.createdByUserId,
                    createdAt: attachment.createdAt,
                    updatedAt: attachment.updatedAt,
                };
            });
        },
        listAttachments: async (issueId) => db
            .select({
            id: issueAttachments.id,
            orgId: issueAttachments.orgId,
            issueId: issueAttachments.issueId,
            issueCommentId: issueAttachments.issueCommentId,
            assetId: issueAttachments.assetId,
            usage: issueAttachments.usage,
            provider: assets.provider,
            objectKey: assets.objectKey,
            contentType: assets.contentType,
            byteSize: assets.byteSize,
            sha256: assets.sha256,
            originalFilename: assets.originalFilename,
            createdByAgentId: assets.createdByAgentId,
            createdByUserId: assets.createdByUserId,
            createdAt: issueAttachments.createdAt,
            updatedAt: issueAttachments.updatedAt,
        })
            .from(issueAttachments)
            .innerJoin(assets, eq(issueAttachments.assetId, assets.id))
            .where(and(eq(issueAttachments.issueId, issueId), eq(issueAttachments.usage, "issue")))
            .orderBy(desc(issueAttachments.createdAt)),
        getAttachmentById: async (id) => db
            .select({
            id: issueAttachments.id,
            orgId: issueAttachments.orgId,
            issueId: issueAttachments.issueId,
            issueCommentId: issueAttachments.issueCommentId,
            assetId: issueAttachments.assetId,
            usage: issueAttachments.usage,
            provider: assets.provider,
            objectKey: assets.objectKey,
            contentType: assets.contentType,
            byteSize: assets.byteSize,
            sha256: assets.sha256,
            originalFilename: assets.originalFilename,
            createdByAgentId: assets.createdByAgentId,
            createdByUserId: assets.createdByUserId,
            createdAt: issueAttachments.createdAt,
            updatedAt: issueAttachments.updatedAt,
        })
            .from(issueAttachments)
            .innerJoin(assets, eq(issueAttachments.assetId, assets.id))
            .where(eq(issueAttachments.id, id))
            .then((rows) => rows[0] ?? null),
        removeAttachment: async (id) => db.transaction(async (tx) => {
            const existing = await tx
                .select({
                id: issueAttachments.id,
                orgId: issueAttachments.orgId,
                issueId: issueAttachments.issueId,
                issueCommentId: issueAttachments.issueCommentId,
                assetId: issueAttachments.assetId,
                usage: issueAttachments.usage,
                provider: assets.provider,
                objectKey: assets.objectKey,
                contentType: assets.contentType,
                byteSize: assets.byteSize,
                sha256: assets.sha256,
                originalFilename: assets.originalFilename,
                createdByAgentId: assets.createdByAgentId,
                createdByUserId: assets.createdByUserId,
                createdAt: issueAttachments.createdAt,
                updatedAt: issueAttachments.updatedAt,
            })
                .from(issueAttachments)
                .innerJoin(assets, eq(issueAttachments.assetId, assets.id))
                .where(eq(issueAttachments.id, id))
                .then((rows) => rows[0] ?? null);
            if (!existing)
                return null;
            await tx.delete(issueAttachments).where(eq(issueAttachments.id, id));
            await tx.delete(assets).where(eq(assets.id, existing.assetId));
            return existing;
        }),
    };
}
//# sourceMappingURL=issues.comments-attachments.js.map