import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { User } from "../../../auth/data/entities.js";
import { Message, MessageObject, MessageRecipient } from "../../data/entities.js";
import { updateDraftSchema } from "../../data/validators.js";
import { buildResolvedMessageActions } from "../../lib/actions.js";
import { MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND } from "../../lib/constants.js";
import { getMessageObjectType } from "../../lib/message-objects-registry.js";
import { getMessageTypeOrDefault } from "../../lib/message-types-registry.js";
import { attachOperationMetadataHeader } from "../../lib/operationMetadata.js";
import { hasOrganizationAccess, resolveMessageContext } from "../../lib/routeHelpers.js";
import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from "../guards.js";
import {
  errorResponseSchema,
  messageDetailResponseSchema,
  okResponseSchema,
  updateDraftSchema as updateDraftOpenApiSchema
} from "../openapi.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("messages").child({ component: "api" });
const metadata = {
  GET: { requireAuth: true },
  PATCH: { requireAuth: true, requireFeatures: ["messages.compose"] },
  DELETE: { requireAuth: true, requireFeatures: ["messages.view"] }
};
async function GET(req, { params }) {
  const { ctx, scope } = await resolveMessageContext(req);
  const em = ctx.container.resolve("em");
  const url = new URL(req.url);
  const skipMarkReadParam = url.searchParams.get("skipMarkRead");
  const skipMarkRead = skipMarkReadParam === "1";
  const message = await findOneWithDecryption(
    em,
    Message,
    {
      id: params.id,
      tenantId: scope.tenantId,
      deletedAt: null
    },
    void 0,
    { tenantId: scope.tenantId, organizationId: scope.organizationId }
  );
  if (!message) {
    return Response.json({ error: "Message not found" }, { status: 404 });
  }
  if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {
    return Response.json({ error: "Access denied" }, { status: 403 });
  }
  const recipient = await em.findOne(MessageRecipient, {
    messageId: params.id,
    recipientUserId: scope.userId,
    deletedAt: null
  });
  const isSender = message.senderUserId === scope.userId;
  const isRecipient = Boolean(recipient);
  if (!isSender && !isRecipient) {
    return Response.json({ error: "Access denied" }, { status: 403 });
  }
  const autoMarkRead = !skipMarkRead && recipient?.status === "unread";
  const readAt = autoMarkRead ? /* @__PURE__ */ new Date() : recipient?.readAt ?? null;
  const objects = await em.find(MessageObject, { messageId: params.id });
  const objectPreviews = await Promise.all(
    objects.map(async (item) => {
      const objectType = getMessageObjectType(item.entityModule, item.entityType);
      if (!objectType?.loadPreview) return null;
      try {
        return await objectType.loadPreview(item.entityId, {
          tenantId: scope.tenantId,
          organizationId: scope.organizationId
        });
      } catch (error) {
        logger.error("Failed to load preview", { entityModule: item.entityModule, entityType: item.entityType, entityId: item.entityId, err: error });
        return null;
      }
    })
  );
  const allRecipients = await em.find(MessageRecipient, { messageId: params.id, deletedAt: null });
  const threadMessages = await findWithDecryption(
    em,
    Message,
    {
      threadId: message.threadId ?? message.id,
      tenantId: scope.tenantId,
      organizationId: scope.organizationId,
      deletedAt: null,
      isDraft: false
    },
    { orderBy: { sentAt: "ASC" } },
    { tenantId: scope.tenantId, organizationId: scope.organizationId }
  );
  const threadMessageIds = threadMessages.map((item) => item.id);
  const visibleRecipientRows = threadMessageIds.length > 0 ? await em.find(MessageRecipient, {
    messageId: { $in: threadMessageIds },
    recipientUserId: scope.userId,
    deletedAt: null
  }) : [];
  const visibleRecipientMessageIds = new Set(visibleRecipientRows.map((item) => item.messageId));
  const actorVisibleThreadMessages = threadMessages.filter((threadMessage) => threadMessage.senderUserId === scope.userId || visibleRecipientMessageIds.has(threadMessage.id));
  const actorRecipientStatusByMessageId = /* @__PURE__ */ new Map();
  for (const row of visibleRecipientRows) {
    actorRecipientStatusByMessageId.set(row.messageId, row.status);
  }
  if (recipient) {
    actorRecipientStatusByMessageId.set(params.id, autoMarkRead ? "read" : recipient.status);
  }
  const actorRecipientStatuses = Array.from(actorRecipientStatusByMessageId.values());
  const conversationArchived = actorRecipientStatuses.length > 0 && actorRecipientStatuses.every((status) => status === "archived");
  const conversationAllUnread = actorRecipientStatuses.length > 0 && actorRecipientStatuses.every((status) => status === "unread");
  const threadSenderIds = actorVisibleThreadMessages.map((threadMessage) => threadMessage.senderUserId).filter((value) => typeof value === "string" && value.length > 0);
  const threadSenders = threadSenderIds.length > 0 ? await findWithDecryption(
    em,
    User,
    { id: { $in: Array.from(new Set(threadSenderIds)) } },
    void 0,
    { tenantId: scope.tenantId, organizationId: scope.organizationId }
  ) : [];
  const threadSenderMap = new Map(threadSenders.map((user) => [user.id, user]));
  const senderUser = await findOneWithDecryption(
    em,
    User,
    { id: message.senderUserId },
    void 0,
    { tenantId: scope.tenantId, organizationId: scope.organizationId }
  );
  const senderName = typeof senderUser?.name === "string" && senderUser.name.trim().length ? senderUser.name.trim() : null;
  const senderEmail = senderUser?.email ?? null;
  const messageType = getMessageTypeOrDefault(message.type);
  const resolvedActionData = buildResolvedMessageActions(message, objects);
  if (autoMarkRead) {
    const commandBus = ctx.container.resolve("commandBus");
    await commandBus.execute("messages.recipients.mark_read", {
      input: {
        messageId: params.id,
        tenantId: scope.tenantId,
        organizationId: scope.organizationId,
        userId: scope.userId
      },
      ctx: {
        container: ctx.container,
        auth: ctx.auth ?? null,
        organizationScope: null,
        selectedOrganizationId: scope.organizationId,
        organizationIds: scope.organizationId ? [scope.organizationId] : null,
        request: req
      }
    });
  }
  return Response.json({
    id: message.id,
    updatedAt: message.updatedAt ?? null,
    type: message.type,
    isDraft: message.isDraft,
    canEditDraft: message.isDraft && message.senderUserId === scope.userId,
    canArchive: Boolean(recipient),
    isArchived: recipient?.status === "archived",
    visibility: message.visibility,
    sourceEntityType: message.sourceEntityType,
    sourceEntityId: message.sourceEntityId,
    externalEmail: message.externalEmail,
    externalName: message.externalName,
    typeDefinition: {
      labelKey: messageType.labelKey,
      icon: messageType.icon,
      color: messageType.color,
      allowReply: messageType.allowReply ?? true,
      allowForward: messageType.allowForward ?? true,
      ui: {
        listItemComponent: messageType.ui?.listItemComponent ?? null,
        contentComponent: messageType.ui?.contentComponent ?? null,
        actionsComponent: messageType.ui?.actionsComponent ?? null
      }
    },
    threadId: message.threadId,
    parentMessageId: message.parentMessageId,
    senderUserId: message.senderUserId,
    senderName,
    senderEmail,
    subject: message.subject,
    body: message.body,
    bodyFormat: message.bodyFormat,
    priority: message.priority,
    sentAt: message.sentAt,
    actionData: resolvedActionData,
    actionTaken: message.actionTaken,
    actionTakenAt: message.actionTakenAt,
    actionTakenByUserId: message.actionTakenByUserId,
    recipients: allRecipients.map((item) => ({
      userId: item.recipientUserId,
      type: item.recipientType,
      status: autoMarkRead && item.recipientUserId === scope.userId ? "read" : item.status,
      readAt: autoMarkRead && item.recipientUserId === scope.userId ? readAt : item.readAt
    })),
    objects: objects.map((item, index) => ({
      id: item.id,
      entityModule: item.entityModule,
      entityType: item.entityType,
      entityId: item.entityId,
      actionRequired: item.actionRequired,
      actionType: item.actionType,
      actionLabel: item.actionLabel,
      snapshot: item.entitySnapshot,
      preview: objectPreviews[index] ?? null
    })),
    thread: actorVisibleThreadMessages.map((threadMessage) => {
      const sender = threadSenderMap.get(threadMessage.senderUserId);
      const threadSenderName = typeof sender?.name === "string" && sender.name.trim().length ? sender.name.trim() : null;
      return {
        id: threadMessage.id,
        senderUserId: threadMessage.senderUserId,
        senderName: threadSenderName,
        senderEmail: sender?.email ?? null,
        body: threadMessage.body,
        bodyFormat: threadMessage.bodyFormat,
        sentAt: threadMessage.sentAt
      };
    }),
    isRead: recipient ? autoMarkRead || recipient.status !== "unread" : true,
    conversationArchived,
    conversationAllUnread
  });
}
async function PATCH(req, { params }) {
  const { ctx, scope } = await resolveMessageContext(req);
  const em = ctx.container.resolve("em").fork();
  const commandBus = ctx.container.resolve("commandBus");
  const body = await req.json().catch(() => ({}));
  const input = updateDraftSchema.parse(body);
  const message = await em.findOne(Message, {
    id: params.id,
    tenantId: scope.tenantId,
    deletedAt: null
  });
  if (!message) {
    return Response.json({ error: "Message not found" }, { status: 404 });
  }
  if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {
    return Response.json({ error: "Access denied" }, { status: 403 });
  }
  if (message.senderUserId !== scope.userId) {
    return Response.json({ error: "Access denied" }, { status: 403 });
  }
  if (!message.isDraft) {
    return Response.json({ error: "Only draft messages can be edited" }, { status: 409 });
  }
  const guardResult = await runMessageMutationGuards(
    ctx.container,
    {
      tenantId: scope.tenantId,
      organizationId: scope.organizationId,
      userId: scope.userId,
      resourceKind: "messages.message",
      resourceId: message.id,
      operation: "update",
      requestMethod: req.method,
      requestHeaders: req.headers,
      mutationPayload: input
    },
    resolveUserFeatures(ctx.auth)
  );
  if (!guardResult.ok) {
    return Response.json(
      guardResult.errorBody ?? { error: "Operation blocked by guard" },
      { status: guardResult.errorStatus ?? 422 }
    );
  }
  try {
    enforceCommandOptimisticLock({
      resourceKind: MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND,
      resourceId: message.id,
      current: message.updatedAt,
      request: req
    });
    const { logEntry } = await commandBus.execute("messages.messages.update_draft", {
      input: {
        ...input,
        messageId: message.id,
        tenantId: scope.tenantId,
        organizationId: scope.organizationId,
        userId: scope.userId
      },
      ctx: {
        container: ctx.container,
        auth: ctx.auth ?? null,
        organizationScope: null,
        selectedOrganizationId: scope.organizationId,
        organizationIds: scope.organizationId ? [scope.organizationId] : null,
        request: req
      }
    });
    const response = Response.json({ ok: true, id: message.id });
    attachOperationMetadataHeader(response, logEntry, {
      resourceKind: "messages.message",
      resourceId: message.id
    });
    await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
      tenantId: scope.tenantId,
      organizationId: scope.organizationId,
      userId: scope.userId,
      resourceKind: "messages.message",
      resourceId: message.id,
      operation: "update",
      requestMethod: req.method,
      requestHeaders: req.headers
    });
    return response;
  } catch (error) {
    if (isCrudHttpError(error)) {
      return Response.json(error.body, { status: error.status });
    }
    if (error instanceof Error) {
      if (error.message === "Message type cannot be created by users") {
        return Response.json({ error: error.message }, { status: 400 });
      }
      if (error.message === "Only draft messages can be edited") {
        return Response.json({ error: error.message }, { status: 409 });
      }
      if (error.message === "Access denied") {
        return Response.json({ error: error.message }, { status: 403 });
      }
    }
    throw error;
  }
}
async function DELETE(req, { params }) {
  const { ctx, scope } = await resolveMessageContext(req);
  const em = ctx.container.resolve("em").fork();
  const commandBus = ctx.container.resolve("commandBus");
  const message = await em.findOne(Message, {
    id: params.id,
    tenantId: scope.tenantId,
    deletedAt: null
  });
  if (!message) {
    return Response.json({ error: "Message not found" }, { status: 404 });
  }
  if (!hasOrganizationAccess(scope.organizationId, message.organizationId)) {
    return Response.json({ error: "Access denied" }, { status: 403 });
  }
  const guardResult = await runMessageMutationGuards(
    ctx.container,
    {
      tenantId: scope.tenantId,
      organizationId: scope.organizationId,
      userId: scope.userId,
      resourceKind: "messages.message",
      resourceId: params.id,
      operation: "delete",
      requestMethod: req.method,
      requestHeaders: req.headers,
      mutationPayload: null
    },
    resolveUserFeatures(ctx.auth)
  );
  if (!guardResult.ok) {
    return Response.json(
      guardResult.errorBody ?? { error: "Operation blocked by guard" },
      { status: guardResult.errorStatus ?? 422 }
    );
  }
  try {
    enforceCommandOptimisticLock({
      resourceKind: MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND,
      resourceId: message.id,
      current: message.updatedAt,
      request: req
    });
    const { logEntry } = await commandBus.execute("messages.messages.delete_for_actor", {
      input: {
        messageId: params.id,
        tenantId: scope.tenantId,
        organizationId: scope.organizationId,
        userId: scope.userId
      },
      ctx: {
        container: ctx.container,
        auth: ctx.auth ?? null,
        organizationScope: null,
        selectedOrganizationId: scope.organizationId,
        organizationIds: scope.organizationId ? [scope.organizationId] : null,
        request: req
      }
    });
    const response = Response.json({ ok: true });
    attachOperationMetadataHeader(response, logEntry, {
      resourceKind: "messages.message",
      resourceId: params.id
    });
    await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
      tenantId: scope.tenantId,
      organizationId: scope.organizationId,
      userId: scope.userId,
      resourceKind: "messages.message",
      resourceId: params.id,
      operation: "delete",
      requestMethod: req.method,
      requestHeaders: req.headers
    });
    return response;
  } catch (error) {
    if (isCrudHttpError(error)) {
      return Response.json(error.body, { status: error.status });
    }
    if (error instanceof Error && error.message === "Access denied") {
      return Response.json({ error: "Access denied" }, { status: 403 });
    }
    throw error;
  }
}
const openApi = {
  tag: "Messages",
  methods: {
    GET: {
      summary: "Get message detail",
      responses: [
        {
          status: 200,
          description: "Message detail with actor-visible thread items only",
          schema: messageDetailResponseSchema
        }
      ],
      errors: [
        { status: 403, description: "Access denied", schema: errorResponseSchema },
        { status: 404, description: "Message not found", schema: errorResponseSchema }
      ]
    },
    PATCH: {
      summary: "Update draft message",
      requestBody: { schema: updateDraftOpenApiSchema },
      responses: [
        { status: 200, description: "Draft updated", schema: okResponseSchema }
      ],
      errors: [
        { status: 403, description: "Access denied", schema: errorResponseSchema },
        { status: 404, description: "Message not found", schema: errorResponseSchema },
        { status: 409, description: "Only drafts can be edited, or the draft was modified concurrently (optimistic lock conflict)", schema: errorResponseSchema }
      ]
    },
    DELETE: {
      summary: "Delete message for current sender/recipient context",
      responses: [
        { status: 200, description: "Message deleted", schema: okResponseSchema }
      ],
      errors: [
        { status: 403, description: "Access denied", schema: errorResponseSchema },
        { status: 404, description: "Message not found", schema: errorResponseSchema },
        { status: 409, description: "Message was modified concurrently (optimistic lock conflict)", schema: errorResponseSchema }
      ]
    }
  }
};
export {
  DELETE,
  GET,
  PATCH,
  metadata,
  openApi
};
//# sourceMappingURL=route.js.map
