/**
 * @fileoverview Shared issue query, state, visibility, and activity helpers
 * used by the issue service and route layer.
 *
 * @see doc/product/domains/issues/state-machines.md - ISSUE.STATE.001 status and review gates
 * @see doc/product/domains/issues/traceability.md - issue evidence and activity traceability
 * @see doc/product/domains/work-routing/attention-and-wakeup-eligibility.md - attention and unread filters
 */
import { activityLog, heartbeatRuns, issueComments, issueFollows, issueLabels, issueReadStates, issues, labels } from "@rudderhq/db";
import { and, asc, eq, inArray, sql } from "drizzle-orm";
import { conflict } from "../errors.js";
export const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"];
export const MAX_ISSUE_COMMENT_PAGE_LIMIT = 500;
export const BOARD_ORDER_STEP = 1000;
export function isUniqueConstraintConflict(error, constraintName) {
    return (typeof error === "object" &&
        error !== null &&
        "code" in error &&
        error.code === "23505" &&
        "constraint" in error &&
        error.constraint === constraintName);
}
export function assertTransition(from, to) {
    if (from === to)
        return;
    if (!ALL_ISSUE_STATUSES.includes(to)) {
        throw conflict(`Unknown issue status: ${to}`);
    }
}
export function applyStatusSideEffects(status, patch) {
    if (!status)
        return patch;
    if (status === "in_progress" && !patch.startedAt) {
        patch.startedAt = new Date();
    }
    if (status === "done") {
        patch.completedAt = new Date();
    }
    if (status === "cancelled") {
        patch.cancelledAt = new Date();
    }
    return patch;
}
export function sameRunLock(checkoutRunId, actorRunId) {
    if (actorRunId)
        return checkoutRunId === actorRunId;
    return checkoutRunId == null;
}
export const TERMINAL_HEARTBEAT_RUN_STATUSES = new Set(["succeeded", "failed", "cancelled", "timed_out"]);
export function escapeLikePattern(value) {
    return value.replace(/[\\%_]/g, "\\$&");
}
export function textContains(value, query) {
    return Boolean(value && value.toLowerCase().includes(query.toLowerCase()));
}
export function buildSearchSnippet(value, query, maxLength = 160) {
    const compact = value.replace(/\s+/g, " ").trim();
    if (compact.length <= maxLength)
        return compact;
    const index = compact.toLowerCase().indexOf(query.toLowerCase());
    if (index < 0)
        return `${compact.slice(0, maxLength - 1).trimEnd()}…`;
    const context = Math.max(20, Math.floor((maxLength - query.length) / 2));
    const start = Math.max(0, index - context);
    const end = Math.min(compact.length, start + maxLength);
    const prefix = start > 0 ? "…" : "";
    const suffix = end < compact.length ? "…" : "";
    return `${prefix}${compact.slice(start, end).trim()}${suffix}`;
}
export function fieldSearchMatch(row, query, searchFields) {
    if (textContains(row.identifier, query)) {
        return { field: "identifier", snippet: row.identifier ?? "" };
    }
    if (searchFields.has("title") && textContains(row.title, query)) {
        return { field: "title", snippet: buildSearchSnippet(row.title, query) };
    }
    if (searchFields.has("description") && textContains(row.description, query)) {
        return { field: "description", snippet: buildSearchSnippet(row.description, query) };
    }
    return null;
}
export function touchedByUserCondition(orgId, userId) {
    return sql `
    (
      ${issues.createdByUserId} = ${userId}
      OR ${issues.assigneeUserId} = ${userId}
      OR ${issues.reviewerUserId} = ${userId}
      OR ${followedByUserCondition(orgId, userId)}
      OR ${automationIssueNotifiedToUserCondition(orgId, userId)}
      OR EXISTS (
        SELECT 1
        FROM ${issueReadStates}
        WHERE ${issueReadStates.issueId} = ${issues.id}
          AND ${issueReadStates.orgId} = ${orgId}
          AND ${issueReadStates.userId} = ${userId}
      )
      OR EXISTS (
        SELECT 1
        FROM ${issueComments}
        WHERE ${issueComments.issueId} = ${issues.id}
          AND ${issueComments.orgId} = ${orgId}
          AND ${issueComments.deletedAt} IS NULL
          AND ${issueComments.authorUserId} = ${userId}
      )
    )
  `;
}
export function followedByUserCondition(orgId, userId) {
    return sql `
    EXISTS (
      SELECT 1
      FROM ${issueFollows}
      WHERE ${issueFollows.issueId} = ${issues.id}
        AND ${issueFollows.orgId} = ${orgId}
        AND ${issueFollows.userId} = ${userId}
    )
  `;
}
export function automationIssueNotifiedToUserCondition(orgId, userId) {
    return sql `
    EXISTS (
      SELECT 1
      FROM ${activityLog}
      WHERE ${activityLog.orgId} = ${orgId}
        AND ${activityLog.entityType} = 'issue'
        AND ${activityLog.entityId} = ${issues.id}::text
        AND ${activityLog.action} = 'automation.issue_created_notification'
        AND ${activityLog.details}->>'userId' = ${userId}
    )
  `;
}
export function automationExecutionVisibleToUserCondition(orgId, userId) {
    return sql `
    (
      ${issues.originKind} <> 'automation_execution'
      OR ${followedByUserCondition(orgId, userId)}
      OR ${automationIssueNotifiedToUserCondition(orgId, userId)}
    )
  `;
}
export function participatedByAgentCondition(orgId, agentId) {
    return sql `
    (
      ${issues.createdByAgentId} = ${agentId}
      OR ${issues.assigneeAgentId} = ${agentId}
      OR ${issues.reviewerAgentId} = ${agentId}
      OR EXISTS (
        SELECT 1
        FROM ${issueComments}
        WHERE ${issueComments.issueId} = ${issues.id}
          AND ${issueComments.orgId} = ${orgId}
          AND ${issueComments.authorAgentId} = ${agentId}
      )
      OR EXISTS (
        SELECT 1
        FROM ${activityLog}
        WHERE ${activityLog.orgId} = ${orgId}
          AND ${activityLog.entityType} = 'issue'
          AND ${activityLog.entityId} = ${issues.id}::text
          AND ${activityLog.agentId} = ${agentId}
      )
    )
  `;
}
export function myLastCommentAtExpr(orgId, userId) {
    return sql `
    (
      SELECT MAX(${issueComments.createdAt})
      FROM ${issueComments}
      WHERE ${issueComments.issueId} = ${issues.id}
        AND ${issueComments.orgId} = ${orgId}
        AND ${issueComments.deletedAt} IS NULL
        AND ${issueComments.authorUserId} = ${userId}
    )
  `;
}
export function myLastReadAtExpr(orgId, userId) {
    return sql `
    (
      SELECT MAX(${issueReadStates.lastReadAt})
      FROM ${issueReadStates}
      WHERE ${issueReadStates.issueId} = ${issues.id}
        AND ${issueReadStates.orgId} = ${orgId}
        AND ${issueReadStates.userId} = ${userId}
    )
  `;
}
export function myLastTouchAtExpr(orgId, userId) {
    const myLastCommentAt = myLastCommentAtExpr(orgId, userId);
    const myLastReadAt = myLastReadAtExpr(orgId, userId);
    return sql `
    GREATEST(
      COALESCE(${myLastCommentAt}, to_timestamp(0)),
      COALESCE(${myLastReadAt}, to_timestamp(0)),
      COALESCE(CASE WHEN ${issues.createdByUserId} = ${userId} THEN ${issues.createdAt} ELSE NULL END, to_timestamp(0)),
      COALESCE(CASE WHEN ${issues.assigneeUserId} = ${userId} THEN ${issues.updatedAt} ELSE NULL END, to_timestamp(0)),
      COALESCE(CASE WHEN ${issues.reviewerUserId} = ${userId} THEN ${issues.updatedAt} ELSE NULL END, to_timestamp(0))
    )
  `;
}
export function unreadForUserCondition(orgId, userId) {
    const touchedCondition = touchedByUserCondition(orgId, userId);
    const myLastTouchAt = myLastTouchAtExpr(orgId, userId);
    return sql `
    (
      ${touchedCondition}
      AND EXISTS (
        SELECT 1
        FROM ${issueComments}
        WHERE ${issueComments.issueId} = ${issues.id}
          AND ${issueComments.orgId} = ${orgId}
          AND ${issueComments.deletedAt} IS NULL
          AND (
            ${issueComments.authorUserId} IS NULL
            OR ${issueComments.authorUserId} <> ${userId}
          )
          AND ${issueComments.createdAt} > ${myLastTouchAt}
      )
    )
  `;
}
export function deriveIssueUserContext(issue, userId, stats) {
    const 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;
    };
    const myLastCommentAt = normalizeDate(stats?.myLastCommentAt);
    const myLastReadAt = normalizeDate(stats?.myLastReadAt);
    const createdTouchAt = issue.createdByUserId === userId ? normalizeDate(issue.createdAt) : null;
    const assignedTouchAt = issue.assigneeUserId === userId ? normalizeDate(issue.updatedAt) : null;
    const reviewerTouchAt = issue.reviewerUserId === userId ? normalizeDate(issue.updatedAt) : null;
    const myLastTouchAt = [myLastCommentAt, myLastReadAt, createdTouchAt, assignedTouchAt, reviewerTouchAt]
        .filter((value) => value instanceof Date)
        .sort((a, b) => b.getTime() - a.getTime())[0] ?? null;
    const lastExternalCommentAt = normalizeDate(stats?.lastExternalCommentAt);
    const isUnreadForMe = Boolean(myLastTouchAt &&
        lastExternalCommentAt &&
        lastExternalCommentAt.getTime() > myLastTouchAt.getTime());
    return {
        myLastTouchAt,
        lastExternalCommentAt,
        isUnreadForMe,
    };
}
export async function labelMapForIssues(dbOrTx, issueIds) {
    const map = new Map();
    if (issueIds.length === 0)
        return map;
    const rows = await dbOrTx
        .select({
        issueId: issueLabels.issueId,
        label: labels,
    })
        .from(issueLabels)
        .innerJoin(labels, eq(issueLabels.labelId, labels.id))
        .where(inArray(issueLabels.issueId, issueIds))
        .orderBy(asc(labels.name), asc(labels.id));
    for (const row of rows) {
        const existing = map.get(row.issueId);
        if (existing)
            existing.push(row.label);
        else
            map.set(row.issueId, [row.label]);
    }
    return map;
}
export async function withIssueLabels(dbOrTx, rows) {
    if (rows.length === 0)
        return [];
    const labelsByIssueId = await labelMapForIssues(dbOrTx, rows.map((row) => row.id));
    return rows.map((row) => {
        const issueLabels = labelsByIssueId.get(row.id) ?? [];
        return {
            ...row,
            labels: issueLabels,
            labelIds: issueLabels.map((label) => label.id),
        };
    });
}
export const ACTIVE_RUN_STATUSES = ["queued", "running"];
export async function activeRunMapForIssues(dbOrTx, issueRows) {
    const map = new Map();
    const runIds = issueRows
        .map((row) => row.executionRunId)
        .filter((id) => id != null);
    if (runIds.length === 0)
        return map;
    const rows = await dbOrTx
        .select({
        id: heartbeatRuns.id,
        status: heartbeatRuns.status,
        agentId: heartbeatRuns.agentId,
        invocationSource: heartbeatRuns.invocationSource,
        triggerDetail: heartbeatRuns.triggerDetail,
        startedAt: heartbeatRuns.startedAt,
        finishedAt: heartbeatRuns.finishedAt,
        createdAt: heartbeatRuns.createdAt,
    })
        .from(heartbeatRuns)
        .where(and(inArray(heartbeatRuns.id, runIds), inArray(heartbeatRuns.status, ACTIVE_RUN_STATUSES)));
    for (const row of rows) {
        map.set(row.id, row);
    }
    return map;
}
export function withActiveRuns(issueRows, runMap) {
    return issueRows.map((row) => ({
        ...row,
        activeRun: row.executionRunId ? (runMap.get(row.executionRunId) ?? null) : null,
    }));
}
//# sourceMappingURL=issues.helpers.js.map