import { isDeepStrictEqual } from "node:util";
import { and, asc, eq, inArray, isNotNull } from "drizzle-orm";
import { documents, heartbeatRuns, issueComments, issueDocuments, issueThreadInteractions, issues, } from "@paperclipai/db";
import { acceptIssueThreadInteractionSchema, askUserQuestionsPayloadSchema, askUserQuestionsResultSchema, cancelIssueThreadInteractionSchema, createIssueThreadInteractionSchema, rejectIssueThreadInteractionSchema, requestCheckboxConfirmationPayloadSchema, requestCheckboxConfirmationResultSchema, requestConfirmationPayloadSchema, requestConfirmationResultSchema, suggestTasksPayloadSchema, suggestTasksResultSchema, } from "@paperclipai/shared";
import { conflict, notFound, unprocessable } from "../errors.js";
import { issueService, listUnfinalizedExecutionWorkspaceIds } from "./issues.js";
const ISSUE_THREAD_INTERACTION_IDEMPOTENCY_CONSTRAINT = "issue_thread_interactions_company_issue_idempotency_uq";
const REQUEST_CONFIRMATION_INTERACTION_KINDS = [
    "request_confirmation",
    "request_checkbox_confirmation",
];
function isRequestConfirmationLikeKind(kind) {
    return REQUEST_CONFIRMATION_INTERACTION_KINDS.includes(kind);
}
function isIssueThreadInteractionIdempotencyConflict(error) {
    if (typeof error !== "object" || error === null)
        return false;
    const err = error;
    const constraint = err.constraint ?? err.constraint_name;
    return err.code === "23505" && constraint === ISSUE_THREAD_INTERACTION_IDEMPOTENCY_CONSTRAINT;
}
function isEquivalentCreateRequest(row, input, actor) {
    return (row.kind === input.kind
        && row.continuationPolicy === input.continuationPolicy
        && (row.idempotencyKey ?? null) === (input.idempotencyKey ?? null)
        && (row.sourceCommentId ?? null) === (input.sourceCommentId ?? null)
        && (row.sourceRunId ?? null) === (input.sourceRunId ?? null)
        && (row.title ?? null) === (input.title ?? null)
        && (row.summary ?? null) === (input.summary ?? null)
        && (row.createdByAgentId ?? null) === (actor.agentId ?? null)
        && (row.createdByUserId ?? null) === (actor.userId ?? null)
        && isDeepStrictEqual(row.payload, input.payload));
}
function hydrateInteraction(row) {
    const base = {
        ...row,
        idempotencyKey: row.idempotencyKey ?? null,
        status: row.status,
        continuationPolicy: row.continuationPolicy,
    };
    switch (row.kind) {
        case "suggest_tasks":
            return {
                ...base,
                kind: "suggest_tasks",
                payload: suggestTasksPayloadSchema.parse(row.payload),
                result: row.result ? suggestTasksResultSchema.parse(row.result) : null,
            };
        case "ask_user_questions":
            return {
                ...base,
                kind: "ask_user_questions",
                payload: askUserQuestionsPayloadSchema.parse(row.payload),
                result: row.result ? askUserQuestionsResultSchema.parse(row.result) : null,
            };
        case "request_confirmation":
            return {
                ...base,
                kind: "request_confirmation",
                payload: requestConfirmationPayloadSchema.parse(row.payload),
                result: row.result ? requestConfirmationResultSchema.parse(row.result) : null,
            };
        case "request_checkbox_confirmation":
            return {
                ...base,
                kind: "request_checkbox_confirmation",
                payload: requestCheckboxConfirmationPayloadSchema.parse(row.payload),
                result: row.result ? requestCheckboxConfirmationResultSchema.parse(row.result) : null,
            };
        default:
            throw unprocessable(`Unknown interaction kind: ${row.kind}`);
    }
}
async function touchIssue(db, issueId) {
    await db
        .update(issues)
        .set({ updatedAt: new Date() })
        .where(eq(issues.id, issueId));
}
function isTerminalIssueStatus(status) {
    return status === "done" || status === "cancelled";
}
function shouldReturnAcceptedConfirmationToCreatorAgent(args) {
    if (!isRequestConfirmationLikeKind(args.current.kind))
        return false;
    if (!args.current.createdByAgentId)
        return false;
    if (!args.actor.userId)
        return false;
    if (!args.issue.assigneeUserId)
        return false;
    if (args.issue.assigneeAgentId)
        return false;
    if (isTerminalIssueStatus(args.issue.status))
        return false;
    return true;
}
function shouldSupersedeRequestConfirmationOnUserComment(interaction) {
    return interaction.payload.supersedeOnUserComment === true;
}
function normalizeCreateInteractionInput(input) {
    switch (input.kind) {
        case "request_confirmation":
            return {
                ...input,
                payload: {
                    ...input.payload,
                    supersedeOnUserComment: input.payload.supersedeOnUserComment ?? true,
                },
            };
        case "request_checkbox_confirmation":
            return {
                ...input,
                payload: {
                    ...input.payload,
                    supersedeOnUserComment: input.payload.supersedeOnUserComment ?? true,
                },
            };
        default:
            return input;
    }
}
function isCommentAtOrAfterInteraction(args) {
    const commentCreatedAtMs = new Date(args.commentCreatedAt).getTime();
    const interactionCreatedAtMs = new Date(args.interactionCreatedAt).getTime();
    if (!Number.isFinite(commentCreatedAtMs) || !Number.isFinite(interactionCreatedAtMs))
        return false;
    return commentCreatedAtMs >= interactionCreatedAtMs;
}
function buildTaskCreationOrder(tasks) {
    const taskByClientKey = new Map(tasks.map((task) => [task.clientKey, task]));
    const ordered = [];
    const state = new Map();
    const visit = (clientKey) => {
        const currentState = state.get(clientKey);
        if (currentState === "done")
            return;
        if (currentState === "visiting") {
            throw unprocessable("Suggested tasks contain a parentClientKey cycle");
        }
        const task = taskByClientKey.get(clientKey);
        if (!task) {
            throw unprocessable(`Unknown parentClientKey: ${clientKey}`);
        }
        state.set(clientKey, "visiting");
        if (task.parentClientKey) {
            visit(task.parentClientKey);
        }
        state.set(clientKey, "done");
        ordered.push(task);
    };
    for (const task of tasks) {
        visit(task.clientKey);
    }
    return ordered;
}
function resolveSelectedSuggestedTasks(args) {
    const taskByClientKey = new Map(args.interaction.payload.tasks.map((task) => [task.clientKey, task]));
    const selectedClientKeys = args.selectedClientKeys ?? args.interaction.payload.tasks.map((task) => task.clientKey);
    const selectedClientKeySet = new Set();
    for (const clientKey of selectedClientKeys) {
        const task = taskByClientKey.get(clientKey);
        if (!task) {
            throw unprocessable(`Unknown suggested task clientKey: ${clientKey}`);
        }
        selectedClientKeySet.add(clientKey);
    }
    if (selectedClientKeySet.size === 0) {
        throw unprocessable("Select at least one suggested task to accept");
    }
    for (const clientKey of selectedClientKeySet) {
        let parentClientKey = taskByClientKey.get(clientKey)?.parentClientKey ?? null;
        while (parentClientKey) {
            if (!selectedClientKeySet.has(parentClientKey)) {
                throw unprocessable(`Suggested task ${clientKey} requires its parent ${parentClientKey} to also be selected`);
            }
            parentClientKey = taskByClientKey.get(parentClientKey)?.parentClientKey ?? null;
        }
    }
    return {
        selectedTasks: args.interaction.payload.tasks.filter((task) => selectedClientKeySet.has(task.clientKey)),
        skippedClientKeys: args.interaction.payload.tasks
            .filter((task) => !selectedClientKeySet.has(task.clientKey))
            .map((task) => task.clientKey),
    };
}
function resolveSelectedCheckboxConfirmationOptions(args) {
    const optionIds = new Set(args.interaction.payload.options.map((option) => option.id));
    const selectedOptionIds = args.selectedOptionIds ?? args.interaction.payload.defaultSelectedOptionIds ?? [];
    const selectedOptionIdSet = new Set();
    for (const optionId of selectedOptionIds) {
        if (!optionIds.has(optionId)) {
            throw unprocessable(`Unknown checkbox confirmation optionId: ${optionId}`);
        }
        selectedOptionIdSet.add(optionId);
    }
    const selectedCount = selectedOptionIdSet.size;
    const minSelected = args.interaction.payload.minSelected ?? 0;
    const maxSelected = args.interaction.payload.maxSelected ?? null;
    if (selectedCount < minSelected) {
        throw unprocessable(`Select at least ${minSelected} checkbox confirmation option(s)`);
    }
    if (maxSelected != null && selectedCount > maxSelected) {
        throw unprocessable(`Select no more than ${maxSelected} checkbox confirmation option(s)`);
    }
    return args.interaction.payload.options
        .filter((option) => selectedOptionIdSet.has(option.id))
        .map((option) => option.id);
}
function normalizeQuestionAnswers(args) {
    const questionById = new Map(args.questions.map((question) => [question.id, question]));
    const answerByQuestionId = new Map();
    for (const answer of args.answers) {
        const question = questionById.get(answer.questionId);
        if (!question) {
            throw unprocessable(`Unknown questionId: ${answer.questionId}`);
        }
        if (answerByQuestionId.has(answer.questionId)) {
            throw unprocessable(`Duplicate answer for questionId: ${answer.questionId}`);
        }
        const uniqueOptionIds = [...new Set(answer.optionIds)];
        const validOptionIds = new Set(question.options.map((option) => option.id));
        for (const optionId of uniqueOptionIds) {
            if (!validOptionIds.has(optionId)) {
                throw unprocessable(`Unknown optionId for question ${answer.questionId}: ${optionId}`);
            }
        }
        if (question.selectionMode === "single" && uniqueOptionIds.length > 1) {
            throw unprocessable(`Question ${answer.questionId} only allows one answer`);
        }
        const otherText = answer.otherText?.trim() ?? "";
        answerByQuestionId.set(answer.questionId, {
            questionId: answer.questionId,
            optionIds: uniqueOptionIds,
            ...(otherText ? { otherText } : {}),
        });
    }
    for (const question of args.questions) {
        const answer = answerByQuestionId.get(question.id);
        if (question.required
            && (!answer || (answer.optionIds.length === 0 && !answer.otherText))) {
            throw unprocessable(`Question ${question.id} requires an answer`);
        }
    }
    return args.questions
        .map((question) => answerByQuestionId.get(question.id))
        .filter((answer) => Boolean(answer));
}
async function getIssueDocumentTargetSnapshot(db, args) {
    if (args.target.type !== "issue_document")
        return null;
    const targetIssueId = args.target.issueId ?? args.issueId;
    const row = await db
        .select({
        issueId: issueDocuments.issueId,
        documentId: issueDocuments.documentId,
        key: issueDocuments.key,
        latestRevisionId: documents.latestRevisionId,
        latestRevisionNumber: documents.latestRevisionNumber,
    })
        .from(issueDocuments)
        .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
        .where(and(eq(issueDocuments.companyId, args.companyId), eq(issueDocuments.issueId, targetIssueId), eq(issueDocuments.key, args.target.key)))
        .then((rows) => rows[0] ?? null);
    if (!row)
        return null;
    if (args.target.documentId && args.target.documentId !== row.documentId)
        return null;
    return row;
}
function buildIssueDocumentTargetFromSnapshot(args) {
    if (!args.snapshot?.latestRevisionId)
        return null;
    return {
        type: "issue_document",
        issueId: args.snapshot.issueId ?? args.issueId,
        documentId: args.snapshot.documentId,
        key: args.snapshot.key,
        revisionId: args.snapshot.latestRevisionId,
        revisionNumber: args.snapshot.latestRevisionNumber,
    };
}
function buildIssueDocumentTargetFromDocument(args) {
    if (!args.document?.latestRevisionId)
        return null;
    return {
        type: "issue_document",
        issueId: args.issueId,
        documentId: args.document.id,
        key: args.document.key,
        revisionId: args.document.latestRevisionId,
        revisionNumber: args.document.latestRevisionNumber ?? null,
    };
}
async function assertRequestConfirmationTargetIsCurrent(db, args) {
    if (!args.target)
        return;
    if (args.target.type !== "issue_document")
        return;
    const snapshot = await getIssueDocumentTargetSnapshot(db, {
        companyId: args.companyId,
        issueId: args.issueId,
        target: args.target,
    });
    if (!snapshot || snapshot.latestRevisionId !== args.target.revisionId) {
        throw unprocessable("request_confirmation target must reference the current issue document revision");
    }
    if (args.target.revisionNumber && snapshot.latestRevisionNumber !== args.target.revisionNumber) {
        throw unprocessable("request_confirmation target revisionNumber must match the current issue document revision");
    }
}
async function expireStaleRequestConfirmationTarget(db, args) {
    if (!isRequestConfirmationLikeKind(args.row.kind) || args.row.status !== "pending")
        return null;
    const interaction = hydrateInteraction(args.row);
    const target = interaction.payload.target ?? null;
    if (!target)
        return null;
    if (target.type !== "issue_document")
        return null;
    const snapshot = await getIssueDocumentTargetSnapshot(db, {
        companyId: args.row.companyId,
        issueId: args.row.issueId,
        target,
    });
    const isCurrent = snapshot
        && snapshot.latestRevisionId === target.revisionId
        && (!target.revisionNumber || snapshot.latestRevisionNumber === target.revisionNumber);
    if (isCurrent)
        return null;
    const now = new Date();
    const currentTarget = buildIssueDocumentTargetFromSnapshot({
        issueId: args.row.issueId,
        snapshot,
    });
    const [updated] = await db
        .update(issueThreadInteractions)
        .set({
        status: "expired",
        payload: currentTarget
            ? {
                ...interaction.payload,
                target: currentTarget,
            }
            : interaction.payload,
        result: {
            version: 1,
            outcome: "stale_target",
            staleTarget: target,
        },
        resolvedByAgentId: args.actor.agentId ?? null,
        resolvedByUserId: args.actor.userId ?? null,
        resolvedAt: now,
        updatedAt: now,
    })
        .where(and(eq(issueThreadInteractions.id, args.row.id), eq(issueThreadInteractions.status, "pending")))
        .returning();
    if (!updated) {
        throw conflict("Interaction has already been resolved");
    }
    await touchIssue(db, args.row.issueId);
    return hydrateInteraction(updated);
}
export function issueThreadInteractionService(db) {
    async function getIdempotentInteraction(args) {
        return db
            .select()
            .from(issueThreadInteractions)
            .where(and(eq(issueThreadInteractions.companyId, args.companyId), eq(issueThreadInteractions.issueId, args.issueId), eq(issueThreadInteractions.idempotencyKey, args.idempotencyKey)))
            .then((rows) => rows[0] ?? null);
    }
    async function assertIssueWorkspaceFinalizedForAccept(args) {
        const executionWorkspaceId = await args.db
            .select({ executionWorkspaceId: issues.executionWorkspaceId })
            .from(issues)
            .where(eq(issues.id, args.issue.id))
            .then((rows) => rows[0]?.executionWorkspaceId ?? null);
        if (!executionWorkspaceId)
            return;
        const unfinalized = await listUnfinalizedExecutionWorkspaceIds(args.db, args.issue.companyId, [executionWorkspaceId]);
        if (!unfinalized.has(executionWorkspaceId))
            return;
        throw conflict("Cannot accept interaction: the issue's most recent run has not completed workspace_finalize. "
            + "Retry once the local worktree has finished syncing.", { executionWorkspaceId });
    }
    async function getPendingInteractionForResolution(args) {
        const current = await db
            .select()
            .from(issueThreadInteractions)
            .where(eq(issueThreadInteractions.id, args.interactionId))
            .then((rows) => rows[0] ?? null);
        if (!current)
            throw notFound("Interaction not found");
        if (current.companyId !== args.issue.companyId || current.issueId !== args.issue.id) {
            throw notFound("Interaction not found");
        }
        if (current.status !== "pending") {
            throw conflict("Interaction has already been resolved");
        }
        return current;
    }
    async function acceptRequestConfirmation(args) {
        const expired = await expireStaleRequestConfirmationTarget(db, {
            row: args.current,
            actor: args.actor,
        });
        if (expired) {
            return { interaction: expired, continuationIssue: null };
        }
        const interaction = hydrateInteraction(args.current);
        const selectedOptionIds = interaction.kind === "request_checkbox_confirmation"
            ? resolveSelectedCheckboxConfirmationOptions({
                interaction,
                selectedOptionIds: args.input.selectedOptionIds,
            })
            : undefined;
        const now = new Date();
        return db.transaction(async (tx) => {
            const [updated] = await tx
                .update(issueThreadInteractions)
                .set({
                status: "accepted",
                result: {
                    version: 1,
                    outcome: "accepted",
                    ...(selectedOptionIds ? { selectedOptionIds } : {}),
                },
                resolvedByAgentId: args.actor.agentId ?? null,
                resolvedByUserId: args.actor.userId ?? null,
                resolvedAt: now,
                updatedAt: now,
            })
                .where(and(eq(issueThreadInteractions.id, args.current.id), eq(issueThreadInteractions.status, "pending")))
                .returning();
            if (!updated) {
                throw conflict("Interaction has already been resolved");
            }
            const issueContext = await tx
                .select({
                id: issues.id,
                companyId: issues.companyId,
                status: issues.status,
                assigneeAgentId: issues.assigneeAgentId,
                assigneeUserId: issues.assigneeUserId,
            })
                .from(issues)
                .where(eq(issues.id, args.issue.id))
                .then((rows) => rows[0] ?? null);
            if (!issueContext || issueContext.companyId !== args.issue.companyId) {
                throw notFound("Issue not found");
            }
            let continuationIssue = null;
            if (shouldReturnAcceptedConfirmationToCreatorAgent({
                issue: issueContext,
                current: args.current,
                actor: args.actor,
            })) {
                const returnStatus = issueContext.status === "blocked" ? "blocked" : "todo";
                const returnedIssue = await issueService(db).update(args.issue.id, {
                    status: returnStatus,
                    assigneeAgentId: args.current.createdByAgentId,
                    assigneeUserId: null,
                    actorAgentId: args.actor.agentId ?? null,
                    actorUserId: args.actor.userId ?? null,
                }, tx);
                if (returnedIssue) {
                    continuationIssue = {
                        id: returnedIssue.id,
                        assigneeAgentId: returnedIssue.assigneeAgentId ?? null,
                        assigneeUserId: returnedIssue.assigneeUserId ?? null,
                        status: returnedIssue.status,
                    };
                }
            }
            else {
                await touchIssue(tx, args.issue.id);
            }
            return {
                interaction: hydrateInteraction(updated),
                continuationIssue,
            };
        });
    }
    async function rejectRequestConfirmation(args) {
        const expired = await expireStaleRequestConfirmationTarget(db, {
            row: args.current,
            actor: args.actor,
        });
        if (expired) {
            return expired;
        }
        const interaction = hydrateInteraction(args.current);
        const reason = args.input.reason?.trim() ?? "";
        if (interaction.payload.rejectRequiresReason === true && reason.length === 0) {
            throw unprocessable("A decline reason is required for this confirmation");
        }
        const now = new Date();
        const [updated] = await db
            .update(issueThreadInteractions)
            .set({
            status: "rejected",
            result: {
                version: 1,
                outcome: "rejected",
                reason: reason || null,
            },
            resolvedByAgentId: args.actor.agentId ?? null,
            resolvedByUserId: args.actor.userId ?? null,
            resolvedAt: now,
            updatedAt: now,
        })
            .where(and(eq(issueThreadInteractions.id, args.current.id), eq(issueThreadInteractions.status, "pending")))
            .returning();
        if (!updated) {
            throw conflict("Interaction has already been resolved");
        }
        await touchIssue(db, args.issue.id);
        return hydrateInteraction(updated);
    }
    return {
        listForIssue: async (issueId) => {
            const rows = await db
                .select()
                .from(issueThreadInteractions)
                .where(eq(issueThreadInteractions.issueId, issueId))
                .orderBy(asc(issueThreadInteractions.createdAt), asc(issueThreadInteractions.id));
            return rows.map((row) => hydrateInteraction(row));
        },
        getById: async (interactionId) => {
            const row = await db
                .select()
                .from(issueThreadInteractions)
                .where(eq(issueThreadInteractions.id, interactionId))
                .then((rows) => rows[0] ?? null);
            return row ? hydrateInteraction(row) : null;
        },
        create: async (issue, input, actor) => {
            const data = normalizeCreateInteractionInput(createIssueThreadInteractionSchema.parse(input));
            if (data.idempotencyKey) {
                const existing = await getIdempotentInteraction({
                    issueId: issue.id,
                    companyId: issue.companyId,
                    idempotencyKey: data.idempotencyKey,
                });
                if (existing) {
                    if (!isEquivalentCreateRequest(existing, data, actor)) {
                        throw conflict("Interaction idempotency key already exists for a different request", {
                            idempotencyKey: data.idempotencyKey,
                        });
                    }
                    return hydrateInteraction(existing);
                }
            }
            if (data.sourceCommentId) {
                const sourceComment = await db
                    .select({
                    companyId: issueComments.companyId,
                    issueId: issueComments.issueId,
                })
                    .from(issueComments)
                    .where(eq(issueComments.id, data.sourceCommentId))
                    .then((rows) => rows[0] ?? null);
                if (!sourceComment || sourceComment.companyId !== issue.companyId || sourceComment.issueId !== issue.id) {
                    throw unprocessable("sourceCommentId must belong to the same issue and company");
                }
            }
            if (data.sourceRunId) {
                const sourceRun = await db
                    .select({
                    companyId: heartbeatRuns.companyId,
                })
                    .from(heartbeatRuns)
                    .where(eq(heartbeatRuns.id, data.sourceRunId))
                    .then((rows) => rows[0] ?? null);
                if (!sourceRun || sourceRun.companyId !== issue.companyId) {
                    throw unprocessable("sourceRunId must belong to the same company");
                }
            }
            if (data.kind === "request_confirmation" || data.kind === "request_checkbox_confirmation") {
                await assertRequestConfirmationTargetIsCurrent(db, {
                    companyId: issue.companyId,
                    issueId: issue.id,
                    target: data.payload.target ?? null,
                });
            }
            let created;
            try {
                [created] = await db
                    .insert(issueThreadInteractions)
                    .values({
                    companyId: issue.companyId,
                    issueId: issue.id,
                    kind: data.kind,
                    status: "pending",
                    continuationPolicy: data.continuationPolicy,
                    idempotencyKey: data.idempotencyKey ?? null,
                    sourceCommentId: data.sourceCommentId ?? null,
                    sourceRunId: data.sourceRunId ?? null,
                    title: data.title ?? null,
                    summary: data.summary ?? null,
                    createdByAgentId: actor.agentId ?? null,
                    createdByUserId: actor.userId ?? null,
                    payload: data.payload,
                })
                    .returning();
            }
            catch (error) {
                if (!data.idempotencyKey || !isIssueThreadInteractionIdempotencyConflict(error)) {
                    throw error;
                }
                const existing = await getIdempotentInteraction({
                    issueId: issue.id,
                    companyId: issue.companyId,
                    idempotencyKey: data.idempotencyKey,
                });
                if (!existing)
                    throw error;
                if (!isEquivalentCreateRequest(existing, data, actor)) {
                    throw conflict("Interaction idempotency key already exists for a different request", {
                        idempotencyKey: data.idempotencyKey,
                    });
                }
                return hydrateInteraction(existing);
            }
            await touchIssue(db, issue.id);
            return hydrateInteraction(created);
        },
        acceptInteraction: async (issue, interactionId, input, actor) => {
            const data = acceptIssueThreadInteractionSchema.parse(input);
            const current = await getPendingInteractionForResolution({ issue, interactionId });
            switch (current.kind) {
                case "suggest_tasks":
                    // Accepting suggest_tasks only creates follow-up issues; it does not
                    // approve code state or move the source workspace forward, so the
                    // workspace_finalize gate (PAPA-440) does not apply here.
                    return issueThreadInteractionService(db).acceptSuggestedTasks(issue, interactionId, data, actor);
                case "request_confirmation": {
                    await assertIssueWorkspaceFinalizedForAccept({ db, issue });
                    const accepted = await acceptRequestConfirmation({
                        issue,
                        current,
                        input: data,
                        actor,
                    });
                    return {
                        interaction: accepted.interaction,
                        continuationIssue: accepted.continuationIssue,
                        createdIssues: [],
                    };
                }
                case "request_checkbox_confirmation": {
                    await assertIssueWorkspaceFinalizedForAccept({ db, issue });
                    const accepted = await acceptRequestConfirmation({
                        issue,
                        current,
                        input: data,
                        actor,
                    });
                    return {
                        interaction: accepted.interaction,
                        continuationIssue: accepted.continuationIssue,
                        createdIssues: [],
                    };
                }
                default:
                    throw unprocessable(`Interactions of kind ${current.kind} cannot be accepted`);
            }
        },
        acceptSuggestedTasks: async (issue, interactionId, input, actor) => {
            const current = await db
                .select()
                .from(issueThreadInteractions)
                .where(eq(issueThreadInteractions.id, interactionId))
                .then((rows) => rows[0] ?? null);
            if (!current)
                throw notFound("Interaction not found");
            if (current.companyId !== issue.companyId || current.issueId !== issue.id) {
                throw notFound("Interaction not found");
            }
            if (current.kind !== "suggest_tasks") {
                throw unprocessable("Only suggest_tasks interactions can be accepted");
            }
            if (current.status !== "pending") {
                throw conflict("Interaction has already been resolved");
            }
            const interaction = hydrateInteraction(current);
            const { selectedTasks, skippedClientKeys } = resolveSelectedSuggestedTasks({
                interaction,
                selectedClientKeys: input.selectedClientKeys,
            });
            const orderedTasks = buildTaskCreationOrder(selectedTasks);
            const explicitParentIds = [...new Set([
                    issue.id,
                    ...(interaction.payload.defaultParentId ? [interaction.payload.defaultParentId] : []),
                    ...selectedTasks
                        .map((task) => task.parentId ?? null)
                        .filter((value) => Boolean(value)),
                ])];
            const parentRows = explicitParentIds.length === 0
                ? []
                : await db
                    .select({
                    id: issues.id,
                    identifier: issues.identifier,
                    companyId: issues.companyId,
                })
                    .from(issues)
                    .where(and(eq(issues.companyId, issue.companyId), inArray(issues.id, explicitParentIds)));
            if (parentRows.length !== explicitParentIds.length) {
                throw unprocessable("Suggested tasks reference parent issues outside this company or issue tree");
            }
            const parentById = new Map(parentRows.map((row) => [row.id, row]));
            const createdByClientKey = new Map();
            const createdWakeTargets = [];
            await db.transaction(async (tx) => {
                const resolvedAt = new Date();
                const [claimed] = await tx
                    .update(issueThreadInteractions)
                    .set({
                    status: "accepted",
                    resolvedByAgentId: actor.agentId ?? null,
                    resolvedByUserId: actor.userId ?? null,
                    resolvedAt,
                    updatedAt: resolvedAt,
                })
                    .where(and(eq(issueThreadInteractions.id, interactionId), eq(issueThreadInteractions.status, "pending")))
                    .returning();
                if (!claimed) {
                    throw conflict("Interaction has already been resolved");
                }
                for (const task of orderedTasks) {
                    const parentIssueId = task.parentClientKey
                        ? createdByClientKey.get(task.parentClientKey)?.issueId ?? null
                        : task.parentId ?? interaction.payload.defaultParentId ?? issue.id;
                    if (!parentIssueId) {
                        throw unprocessable(`Unable to resolve parent for suggested task ${task.clientKey}`);
                    }
                    const { issue: createdIssue } = await issueService(tx).createChild(parentIssueId, {
                        title: task.title,
                        description: task.description ?? null,
                        status: "todo",
                        workMode: task.workMode ?? "standard",
                        priority: task.priority ?? "medium",
                        assigneeAgentId: task.assigneeAgentId ?? null,
                        assigneeUserId: task.assigneeUserId ?? null,
                        projectId: task.projectId ?? issue.projectId,
                        goalId: task.goalId ?? issue.goalId,
                        billingCode: task.billingCode ?? null,
                        createdByAgentId: actor.agentId ?? null,
                        createdByUserId: actor.userId ?? null,
                        actorAgentId: actor.agentId ?? null,
                        actorUserId: actor.userId ?? null,
                    });
                    const parentIdentifier = createdByClientKey.get(task.parentClientKey ?? "")?.identifier
                        ?? parentById.get(parentIssueId)?.identifier
                        ?? null;
                    createdByClientKey.set(task.clientKey, {
                        clientKey: task.clientKey,
                        issueId: createdIssue.id,
                        identifier: createdIssue.identifier ?? null,
                        title: createdIssue.title,
                        parentIssueId,
                        parentIdentifier,
                    });
                    createdWakeTargets.push({
                        id: createdIssue.id,
                        assigneeAgentId: createdIssue.assigneeAgentId ?? null,
                        status: createdIssue.status,
                    });
                }
                const [updated] = await tx
                    .update(issueThreadInteractions)
                    .set({
                    result: {
                        version: 1,
                        createdTasks: [...createdByClientKey.values()],
                        ...(skippedClientKeys.length > 0 ? { skippedClientKeys } : {}),
                    },
                    updatedAt: new Date(),
                })
                    .where(eq(issueThreadInteractions.id, interactionId))
                    .returning();
                await touchIssue(tx, issue.id);
                current.status = updated.status;
                current.result = updated.result;
                current.resolvedByAgentId = updated.resolvedByAgentId;
                current.resolvedByUserId = updated.resolvedByUserId;
                current.resolvedAt = updated.resolvedAt;
                current.updatedAt = updated.updatedAt;
            });
            return {
                interaction: hydrateInteraction(current),
                createdIssues: createdWakeTargets,
            };
        },
        rejectInteraction: async (issue, interactionId, input, actor) => {
            const data = rejectIssueThreadInteractionSchema.parse(input);
            const current = await getPendingInteractionForResolution({ issue, interactionId });
            switch (current.kind) {
                case "suggest_tasks":
                    return issueThreadInteractionService(db).rejectSuggestedTasks(issue, interactionId, data, actor, current);
                case "request_confirmation":
                case "request_checkbox_confirmation":
                    return rejectRequestConfirmation({
                        issue,
                        current,
                        input: data,
                        actor,
                    });
                default:
                    throw unprocessable(`Interactions of kind ${current.kind} cannot be rejected`);
            }
        },
        rejectSuggestedTasks: async (issue, interactionId, input, actor, current) => {
            if (current.companyId !== issue.companyId || current.issueId !== issue.id) {
                throw notFound("Interaction not found");
            }
            if (current.kind !== "suggest_tasks") {
                throw unprocessable("Only suggest_tasks interactions can be rejected");
            }
            if (current.status !== "pending") {
                throw conflict("Interaction has already been resolved");
            }
            const [updated] = await db
                .update(issueThreadInteractions)
                .set({
                status: "rejected",
                result: {
                    version: 1,
                    rejectionReason: input.reason?.trim() || null,
                },
                resolvedByAgentId: actor.agentId ?? null,
                resolvedByUserId: actor.userId ?? null,
                resolvedAt: new Date(),
                updatedAt: new Date(),
            })
                .where(and(eq(issueThreadInteractions.id, interactionId), eq(issueThreadInteractions.status, "pending")))
                .returning();
            if (!updated) {
                throw conflict("Interaction has already been resolved");
            }
            await touchIssue(db, issue.id);
            return hydrateInteraction(updated);
        },
        expireRequestConfirmationsSupersededByComment: async (issue, comment, actor) => {
            if (!comment.authorUserId)
                return [];
            const rows = await db
                .select()
                .from(issueThreadInteractions)
                .where(and(eq(issueThreadInteractions.companyId, issue.companyId), eq(issueThreadInteractions.issueId, issue.id), inArray(issueThreadInteractions.kind, [...REQUEST_CONFIRMATION_INTERACTION_KINDS]), eq(issueThreadInteractions.status, "pending")));
            const superseded = rows.filter((row) => {
                const interaction = hydrateInteraction(row);
                return (shouldSupersedeRequestConfirmationOnUserComment(interaction)
                    && isCommentAtOrAfterInteraction({
                        commentCreatedAt: comment.createdAt,
                        interactionCreatedAt: row.createdAt,
                    }));
            });
            if (superseded.length === 0)
                return [];
            const now = new Date();
            const expired = [];
            for (const row of superseded) {
                const [updated] = await db
                    .update(issueThreadInteractions)
                    .set({
                    status: "expired",
                    result: {
                        version: 1,
                        outcome: "superseded_by_comment",
                        commentId: comment.id,
                    },
                    resolvedByAgentId: actor.agentId ?? null,
                    resolvedByUserId: actor.userId ?? null,
                    resolvedAt: now,
                    updatedAt: now,
                })
                    .where(and(eq(issueThreadInteractions.id, row.id), eq(issueThreadInteractions.status, "pending")))
                    .returning();
                if (updated)
                    expired.push(hydrateInteraction(updated));
            }
            if (expired.length > 0) {
                await touchIssue(db, issue.id);
            }
            return expired;
        },
        expireRequestConfirmationsSupersededByHistoricalComments: async (issue) => {
            const [rows, comments] = await Promise.all([
                db
                    .select()
                    .from(issueThreadInteractions)
                    .where(and(eq(issueThreadInteractions.companyId, issue.companyId), eq(issueThreadInteractions.issueId, issue.id), inArray(issueThreadInteractions.kind, [...REQUEST_CONFIRMATION_INTERACTION_KINDS]), eq(issueThreadInteractions.status, "pending"))),
                db
                    .select()
                    .from(issueComments)
                    .where(and(eq(issueComments.companyId, issue.companyId), eq(issueComments.issueId, issue.id), isNotNull(issueComments.authorUserId)))
                    .orderBy(asc(issueComments.createdAt)),
            ]);
            if (rows.length === 0 || comments.length === 0)
                return [];
            const now = new Date();
            const expired = [];
            const supersededByComment = new Map();
            for (const row of rows) {
                const interaction = hydrateInteraction(row);
                if (!shouldSupersedeRequestConfirmationOnUserComment(interaction))
                    continue;
                const supersedingComment = comments.find((comment) => isCommentAtOrAfterInteraction({
                    commentCreatedAt: comment.createdAt,
                    interactionCreatedAt: row.createdAt,
                }));
                if (!supersedingComment)
                    continue;
                const group = supersededByComment.get(supersedingComment.id);
                if (group) {
                    group.rowIds.push(row.id);
                }
                else {
                    supersededByComment.set(supersedingComment.id, {
                        comment: supersedingComment,
                        rowIds: [row.id],
                    });
                }
            }
            for (const { comment, rowIds } of supersededByComment.values()) {
                const updatedRows = await db
                    .update(issueThreadInteractions)
                    .set({
                    status: "expired",
                    result: {
                        version: 1,
                        outcome: "superseded_by_comment",
                        commentId: comment.id,
                    },
                    resolvedByAgentId: null,
                    resolvedByUserId: comment.authorUserId,
                    resolvedAt: now,
                    updatedAt: now,
                })
                    .where(and(inArray(issueThreadInteractions.id, rowIds), eq(issueThreadInteractions.status, "pending")))
                    .returning();
                expired.push(...updatedRows.map(hydrateInteraction));
            }
            if (expired.length > 0) {
                await touchIssue(db, issue.id);
            }
            return expired;
        },
        expireStaleRequestConfirmationsForIssueDocument: async (issue, document, actor) => {
            const rows = await db
                .select()
                .from(issueThreadInteractions)
                .where(and(eq(issueThreadInteractions.companyId, issue.companyId), eq(issueThreadInteractions.issueId, issue.id), inArray(issueThreadInteractions.kind, [...REQUEST_CONFIRMATION_INTERACTION_KINDS]), eq(issueThreadInteractions.status, "pending")));
            const staleRows = rows.filter((row) => {
                const interaction = hydrateInteraction(row);
                const target = interaction.payload.target;
                if (!target || target.type !== "issue_document")
                    return false;
                const targetIssueId = target.issueId ?? issue.id;
                if (targetIssueId !== issue.id)
                    return false;
                if (document && target.documentId && target.documentId !== document.id)
                    return false;
                if (document && target.key !== document.key)
                    return false;
                if (!document)
                    return true;
                return (target.revisionId !== document.latestRevisionId
                    || (target.revisionNumber != null && target.revisionNumber !== document.latestRevisionNumber));
            });
            if (staleRows.length === 0)
                return [];
            const now = new Date();
            const expired = [];
            for (const row of staleRows) {
                const interaction = hydrateInteraction(row);
                const target = interaction.payload.target ?? null;
                const currentTarget = buildIssueDocumentTargetFromDocument({
                    issueId: issue.id,
                    document,
                });
                const [updated] = await db
                    .update(issueThreadInteractions)
                    .set({
                    status: "expired",
                    payload: currentTarget
                        ? {
                            ...interaction.payload,
                            target: currentTarget,
                        }
                        : interaction.payload,
                    result: {
                        version: 1,
                        outcome: "stale_target",
                        staleTarget: target,
                    },
                    resolvedByAgentId: actor.agentId ?? null,
                    resolvedByUserId: actor.userId ?? null,
                    resolvedAt: now,
                    updatedAt: now,
                })
                    .where(and(eq(issueThreadInteractions.id, row.id), eq(issueThreadInteractions.status, "pending")))
                    .returning();
                if (updated)
                    expired.push(hydrateInteraction(updated));
            }
            if (expired.length > 0) {
                await touchIssue(db, issue.id);
            }
            return expired;
        },
        answerQuestions: async (issue, interactionId, input, actor) => {
            const current = await db
                .select()
                .from(issueThreadInteractions)
                .where(eq(issueThreadInteractions.id, interactionId))
                .then((rows) => rows[0] ?? null);
            if (!current)
                throw notFound("Interaction not found");
            if (current.companyId !== issue.companyId || current.issueId !== issue.id) {
                throw notFound("Interaction not found");
            }
            if (current.kind !== "ask_user_questions") {
                throw unprocessable("Only ask_user_questions interactions can be answered");
            }
            if (current.status !== "pending") {
                throw conflict("Interaction has already been resolved");
            }
            const interaction = hydrateInteraction(current);
            const normalizedAnswers = normalizeQuestionAnswers({
                questions: interaction.payload.questions,
                answers: input.answers,
            });
            const [updated] = await db
                .update(issueThreadInteractions)
                .set({
                status: "answered",
                result: {
                    version: 1,
                    answers: normalizedAnswers,
                    summaryMarkdown: input.summaryMarkdown ?? null,
                },
                resolvedByAgentId: actor.agentId ?? null,
                resolvedByUserId: actor.userId ?? null,
                resolvedAt: new Date(),
                updatedAt: new Date(),
            })
                .where(and(eq(issueThreadInteractions.id, interactionId), eq(issueThreadInteractions.status, "pending")))
                .returning();
            if (!updated) {
                throw conflict("Interaction has already been resolved");
            }
            await touchIssue(db, issue.id);
            return hydrateInteraction(updated);
        },
        cancelQuestions: async (issue, interactionId, input, actor) => {
            const data = cancelIssueThreadInteractionSchema.parse(input);
            const current = await db
                .select()
                .from(issueThreadInteractions)
                .where(eq(issueThreadInteractions.id, interactionId))
                .then((rows) => rows[0] ?? null);
            if (!current)
                throw notFound("Interaction not found");
            if (current.companyId !== issue.companyId || current.issueId !== issue.id) {
                throw notFound("Interaction not found");
            }
            if (current.kind !== "ask_user_questions") {
                throw unprocessable("Only ask_user_questions interactions can be cancelled");
            }
            if (current.status !== "pending") {
                throw conflict("Interaction has already been resolved");
            }
            const reason = data.reason?.trim() || null;
            const [updated] = await db
                .update(issueThreadInteractions)
                .set({
                status: "cancelled",
                result: {
                    version: 1,
                    answers: [],
                    cancelled: true,
                    cancellationReason: reason,
                    summaryMarkdown: null,
                },
                resolvedByAgentId: actor.agentId ?? null,
                resolvedByUserId: actor.userId ?? null,
                resolvedAt: new Date(),
                updatedAt: new Date(),
            })
                .where(and(eq(issueThreadInteractions.id, interactionId), eq(issueThreadInteractions.status, "pending")))
                .returning();
            if (!updated) {
                throw conflict("Interaction has already been resolved");
            }
            await touchIssue(db, issue.id);
            return hydrateInteraction(updated);
        },
    };
}
//# sourceMappingURL=issue-thread-interactions.js.map