import { z } from "zod";
import { registerCommand } from "@open-mercato/shared/lib/commands";
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { Message, MessageObject, MessageRecipient } from "../data/entities.js";
import { emitMessagesEvent } from "../events.js";
import {
  composeMessageSchema,
  forwardMessageSchema,
  replyMessageSchema,
  updateDraftSchema
} from "../data/validators.js";
import { linkAttachmentsToMessage, linkLibraryAttachmentsToMessage, copyAttachmentsForForwardMessages } from "../lib/attachments.js";
import { MESSAGE_ATTACHMENT_ENTITY_ID, MESSAGE_ENTITY_ID } from "../lib/constants.js";
import { getMessageTypeOrDefault } from "../lib/message-types-registry.js";
import { validateMessageObjectsForType } from "../lib/object-validation.js";
import { buildForwardBodyFromLegacyInput, buildForwardPreviewFromThreadSlice, buildForwardThreadSlice } from "../lib/forwarding.js";
import {
  assertOrganizationAccess,
  loadMessageAggregateSnapshot,
  restoreMessageAggregateSnapshot
} from "./shared.js";
async function emitMessageSentEvent(_container, payload) {
  await emitMessagesEvent("messages.message.sent", payload, { persistent: true });
}
async function emitMessageDeletedEvent(_container, payload) {
  await emitMessagesEvent(
    "messages.message.deleted",
    { ...payload, recipientUserId: payload.actorUserId },
    { persistent: true }
  );
}
async function emitMessageGloballyDeletedEvent(_container, payload) {
  const audience = Array.from(/* @__PURE__ */ new Set([payload.actorUserId, ...payload.recipientUserIds]));
  await emitMessagesEvent(
    "messages.message.deleted",
    {
      messageId: payload.messageId,
      actorUserId: payload.actorUserId,
      target: "global",
      recipientUserIds: audience,
      tenantId: payload.tenantId,
      organizationId: payload.organizationId
    },
    { persistent: true }
  );
}
async function emitMessageIndexUpsert(container, payload) {
  let bus = null;
  try {
    bus = container.resolve("eventBus");
  } catch {
    bus = null;
  }
  if (!bus) return;
  await bus.emitEvent(
    "query_index.upsert_one",
    {
      entityType: MESSAGE_ENTITY_ID,
      recordId: payload.messageId,
      tenantId: payload.tenantId,
      organizationId: payload.organizationId,
      crudAction: "updated",
      coverageBaseDelta: 1
    },
    {
      tenantId: payload.tenantId,
      organizationId: payload.organizationId
    }
  ).catch(() => void 0);
}
const scopeSchema = z.object({
  tenantId: z.string().uuid(),
  organizationId: z.string().uuid().nullable(),
  userId: z.string().uuid()
});
const composeCommandSchema = composeMessageSchema.safeExtend({
  tenantId: scopeSchema.shape.tenantId,
  organizationId: scopeSchema.shape.organizationId,
  userId: scopeSchema.shape.userId,
  // Optional dedup key (inbound email ingest sets it; other callers leave it
  // undefined). When set, a re-issued compose returns the first message instead
  // of creating a duplicate.
  idempotencyKey: z.string().min(1).max(255).optional()
});
const updateDraftCommandSchema = updateDraftSchema.safeExtend({
  messageId: z.string().uuid(),
  tenantId: scopeSchema.shape.tenantId,
  organizationId: scopeSchema.shape.organizationId,
  userId: scopeSchema.shape.userId
});
const replyCommandSchema = replyMessageSchema.safeExtend({
  messageId: z.string().uuid(),
  tenantId: scopeSchema.shape.tenantId,
  organizationId: scopeSchema.shape.organizationId,
  userId: scopeSchema.shape.userId
});
const forwardCommandSchema = forwardMessageSchema.safeExtend({
  messageId: z.string().uuid(),
  tenantId: scopeSchema.shape.tenantId,
  organizationId: scopeSchema.shape.organizationId,
  userId: scopeSchema.shape.userId
});
const deleteForActorCommandSchema = z.object({
  messageId: z.string().uuid(),
  tenantId: scopeSchema.shape.tenantId,
  organizationId: scopeSchema.shape.organizationId,
  userId: scopeSchema.shape.userId
});
function toIso(value) {
  return value ? value.toISOString() : null;
}
function toDate(value) {
  if (!value) return null;
  return new Date(value);
}
function buildReplySubject(subject) {
  if (/^re:\s*/i.test(subject)) return subject;
  return `Re: ${subject}`;
}
async function requireMessageById(em, scope, messageId) {
  const message = await findOneWithDecryption(
    em,
    Message,
    {
      id: messageId,
      tenantId: scope.tenantId,
      deletedAt: null
    },
    void 0,
    {
      tenantId: scope.tenantId,
      organizationId: scope.organizationId
    }
  );
  if (!message) throw new Error("Message not found");
  assertOrganizationAccess(scope, message);
  return message;
}
function isUniqueViolation(err) {
  if (!err || typeof err !== "object") return false;
  const code = err.code;
  if (code === "23505") return true;
  const message = err.message;
  return typeof message === "string" && /duplicate key value|unique constraint/i.test(message);
}
async function buildComposeResultFromExisting(em, message) {
  const recipients = await findWithDecryption(
    em,
    MessageRecipient,
    { messageId: message.id, deletedAt: null },
    void 0,
    { tenantId: message.tenantId, organizationId: message.organizationId ?? null }
  );
  return {
    id: message.id,
    threadId: message.threadId ?? null,
    externalEmail: message.externalEmail ?? null,
    isDraft: message.isDraft,
    recipientUserIds: recipients.map((recipient) => recipient.recipientUserId),
    deduplicated: true
  };
}
const composeMessageCommand = {
  id: "messages.messages.compose",
  async execute(rawInput, ctx) {
    const input = composeCommandSchema.parse(rawInput);
    if (input.objects?.length) {
      const objectValidationError = validateMessageObjectsForType(input.type, input.objects);
      if (objectValidationError) throw new Error(objectValidationError);
    }
    const em = ctx.container.resolve("em").fork();
    const scope = { tenantId: input.tenantId, organizationId: input.organizationId };
    if (input.idempotencyKey) {
      const existing = await findOneWithDecryption(
        em,
        Message,
        { tenantId: input.tenantId, idempotencyKey: input.idempotencyKey },
        void 0,
        scope
      );
      if (existing) return buildComposeResultFromExisting(em, existing);
    }
    let messageId = "";
    let responseThreadId = null;
    let responseExternalEmail = null;
    const composeTx = em.transactional(async (trx) => {
      const threadId = input.parentMessageId ? (await findOneWithDecryption(
        trx,
        Message,
        {
          id: input.parentMessageId,
          tenantId: input.tenantId,
          organizationId: input.organizationId,
          deletedAt: null
        },
        void 0,
        scope
      ))?.threadId ?? input.parentMessageId : void 0;
      const isPublicVisibility = input.visibility === "public";
      const sendViaEmail = isPublicVisibility ? true : input.sendViaEmail;
      const message = trx.create(Message, {
        type: input.type,
        visibility: input.visibility ?? null,
        sourceEntityType: input.sourceEntityType,
        sourceEntityId: input.sourceEntityId,
        externalEmail: input.externalEmail,
        externalName: input.externalName,
        threadId: threadId ?? void 0,
        parentMessageId: input.parentMessageId,
        senderUserId: input.userId,
        subject: input.subject,
        body: input.body,
        bodyFormat: input.bodyFormat,
        priority: input.priority,
        status: input.isDraft ? "draft" : "sent",
        isDraft: input.isDraft ?? false,
        sentAt: input.isDraft ? null : /* @__PURE__ */ new Date(),
        actionData: input.actionData,
        sendViaEmail,
        idempotencyKey: input.idempotencyKey ?? null,
        tenantId: input.tenantId,
        organizationId: input.organizationId
      });
      await trx.persist(message).flush();
      if (!threadId && !input.isDraft && !message.threadId) {
        message.threadId = message.id;
        await trx.flush();
      }
      for (const recipient of input.recipients) {
        trx.persist(trx.create(MessageRecipient, {
          messageId: message.id,
          recipientUserId: recipient.userId,
          recipientType: recipient.type,
          status: "unread"
        }));
      }
      if (input.objects) {
        for (const obj of input.objects) {
          trx.persist(trx.create(MessageObject, {
            messageId: message.id,
            entityModule: obj.entityModule,
            entityType: obj.entityType,
            entityId: obj.entityId,
            actionRequired: obj.actionRequired,
            actionType: obj.actionType,
            actionLabel: obj.actionLabel
          }));
        }
      }
      await trx.flush();
      if (input.attachmentIds?.length) {
        await linkAttachmentsToMessage(
          trx,
          message.id,
          input.attachmentIds,
          input.organizationId,
          input.tenantId
        );
      }
      if (input.attachmentRecordId) {
        await linkLibraryAttachmentsToMessage(
          trx,
          message.id,
          input.attachmentRecordId,
          input.organizationId,
          input.tenantId
        );
      }
      messageId = message.id;
      responseThreadId = message.threadId ?? null;
      responseExternalEmail = message.externalEmail ?? null;
    });
    try {
      await composeTx;
    } catch (err) {
      if (input.idempotencyKey && isUniqueViolation(err)) {
        const existing = await findOneWithDecryption(
          em.fork(),
          Message,
          { tenantId: input.tenantId, idempotencyKey: input.idempotencyKey },
          void 0,
          scope
        );
        if (existing) return buildComposeResultFromExisting(em.fork(), existing);
      }
      throw err;
    }
    if (!input.isDraft) {
      await emitMessageSentEvent(ctx.container, {
        messageId,
        senderUserId: input.userId,
        recipientUserIds: input.recipients.map((recipient) => recipient.userId),
        sendViaEmail: input.visibility === "public" ? true : input.sendViaEmail,
        externalEmail: responseExternalEmail,
        tenantId: input.tenantId,
        organizationId: input.organizationId
      });
    }
    await emitMessageIndexUpsert(ctx.container, {
      messageId,
      tenantId: input.tenantId,
      organizationId: input.organizationId
    });
    return {
      id: messageId,
      threadId: responseThreadId,
      externalEmail: responseExternalEmail,
      isDraft: input.isDraft,
      recipientUserIds: input.recipients.map((recipient) => recipient.userId)
    };
  },
  async captureAfter(_input, result, ctx) {
    const em = ctx.container.resolve("em").fork();
    return loadMessageAggregateSnapshot(em, result.id);
  },
  buildLog: async ({ input, result, snapshots }) => {
    if (result.deduplicated) return { skipLog: true };
    const parsed = composeCommandSchema.parse(input);
    return {
      actionLabel: parsed.isDraft ? "Create draft message" : "Compose message",
      resourceKind: "messages.message",
      resourceId: result.id,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      payload: {
        undo: {
          after: snapshots.after ?? null
        }
      },
      snapshotAfter: snapshots.after ?? null
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const after = undo?.after;
    if (!after) return;
    const em = ctx.container.resolve("em").fork();
    const message = await em.findOne(Message, { id: after.message.id });
    if (!message) return;
    message.deletedAt = /* @__PURE__ */ new Date();
    await em.flush();
    if (!after.message.isDraft) {
      await emitMessageGloballyDeletedEvent(ctx.container, {
        messageId: after.message.id,
        actorUserId: after.message.senderUserId,
        recipientUserIds: after.recipients.map((recipient) => recipient.recipientUserId),
        tenantId: after.message.tenantId,
        organizationId: after.message.organizationId
      });
    }
  }
};
const updateDraftCommand = {
  id: "messages.messages.update_draft",
  async prepare(rawInput, ctx) {
    const input = updateDraftCommandSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const snapshot = await loadMessageAggregateSnapshot(em, input.messageId, {
      tenantId: input.tenantId,
      organizationId: input.organizationId
    });
    return snapshot ? { before: snapshot } : {};
  },
  async execute(rawInput, ctx) {
    const input = updateDraftCommandSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const message = await requireMessageById(em, input, input.messageId);
    if (message.senderUserId !== input.userId) throw new Error("Access denied");
    if (!message.isDraft) throw new Error("Only draft messages can be edited");
    const isSending = input.isDraft === false;
    const preloadedRecipients = isSending && !input.recipients ? await findWithDecryption(
      em,
      MessageRecipient,
      { messageId: message.id, deletedAt: null },
      void 0,
      {
        tenantId: input.tenantId,
        organizationId: input.organizationId
      }
    ) : null;
    const nextMessageType = input.type ?? message.type;
    if (input.objects) {
      const objectValidationError = validateMessageObjectsForType(nextMessageType, input.objects);
      if (objectValidationError) throw new Error(objectValidationError);
    } else if (input.type !== void 0) {
      const existingObjects = await em.find(MessageObject, { messageId: message.id });
      if (existingObjects.length > 0) {
        const objectValidationError = validateMessageObjectsForType(
          nextMessageType,
          existingObjects.map((item) => ({
            entityModule: item.entityModule,
            entityType: item.entityType,
            entityId: item.entityId
          }))
        );
        if (objectValidationError) throw new Error(objectValidationError);
      }
    }
    if (isSending) {
      const finalVisibility = input.visibility ?? message.visibility;
      const finalSubject = input.subject ?? message.subject;
      const finalBody = input.body ?? message.body;
      const finalRecipientCount = input.recipients ? input.recipients.length : preloadedRecipients?.length ?? 0;
      if (finalVisibility !== "public" && finalRecipientCount === 0) {
        throw new Error("at least one recipient is required");
      }
      if (!finalSubject?.trim()) throw new Error("subject is required");
      if (!finalBody?.trim()) throw new Error("body is required");
    }
    await withAtomicFlush(em, [() => {
      if (input.type !== void 0) message.type = input.type;
      if (input.visibility !== void 0) message.visibility = input.visibility;
      if (input.sourceEntityType !== void 0) message.sourceEntityType = input.sourceEntityType;
      if (input.sourceEntityId !== void 0) message.sourceEntityId = input.sourceEntityId;
      if (input.externalEmail !== void 0) message.externalEmail = input.externalEmail;
      if (input.externalName !== void 0) message.externalName = input.externalName;
      if (input.subject !== void 0) message.subject = input.subject;
      if (input.body !== void 0) message.body = input.body;
      if (input.bodyFormat !== void 0) message.bodyFormat = input.bodyFormat;
      if (input.priority !== void 0) message.priority = input.priority;
      if (input.actionData !== void 0) message.actionData = input.actionData;
      if (input.sendViaEmail !== void 0) message.sendViaEmail = input.sendViaEmail;
    }, async () => {
      if (input.recipients) {
        await em.nativeDelete(MessageRecipient, { messageId: message.id });
        for (const recipient of input.recipients) {
          em.persist(em.create(MessageRecipient, {
            messageId: message.id,
            recipientUserId: recipient.userId,
            recipientType: recipient.type,
            status: "unread"
          }));
        }
      }
      if (input.objects) {
        await em.nativeDelete(MessageObject, { messageId: message.id });
        for (const object of input.objects) {
          em.persist(em.create(MessageObject, {
            messageId: message.id,
            entityModule: object.entityModule,
            entityType: object.entityType,
            entityId: object.entityId,
            actionRequired: object.actionRequired,
            actionType: object.actionType,
            actionLabel: object.actionLabel
          }));
        }
      }
      if (input.attachmentIds) {
        const { Attachment } = await import("@open-mercato/core/modules/attachments/data/entities");
        if (input.attachmentIds.length === 0) {
          await em.nativeDelete(Attachment, {
            entityId: MESSAGE_ATTACHMENT_ENTITY_ID,
            recordId: message.id,
            tenantId: input.tenantId,
            organizationId: input.organizationId
          });
        } else {
          await em.nativeDelete(Attachment, {
            entityId: MESSAGE_ATTACHMENT_ENTITY_ID,
            recordId: message.id,
            tenantId: input.tenantId,
            organizationId: input.organizationId,
            id: { $nin: input.attachmentIds }
          });
        }
        await linkAttachmentsToMessage(
          em,
          message.id,
          input.attachmentIds,
          input.organizationId,
          input.tenantId
        );
      }
      if (isSending) {
        message.isDraft = false;
        message.status = "sent";
        message.sentAt = /* @__PURE__ */ new Date();
        if (!message.threadId) message.threadId = message.id;
      }
    }], { transaction: true });
    await emitMessageIndexUpsert(ctx.container, {
      messageId: message.id,
      tenantId: input.tenantId,
      organizationId: input.organizationId
    });
    if (isSending) {
      const recipientUserIds = input.recipients ? input.recipients.map((r) => r.userId) : (preloadedRecipients ?? []).map((r) => r.recipientUserId);
      const resolvedVisibility = input.visibility ?? message.visibility;
      const resolvedSendViaEmail = input.sendViaEmail ?? message.sendViaEmail;
      await emitMessageSentEvent(ctx.container, {
        messageId: message.id,
        senderUserId: input.userId,
        recipientUserIds,
        sendViaEmail: resolvedVisibility === "public" ? true : resolvedSendViaEmail,
        externalEmail: message.externalEmail ?? null,
        tenantId: input.tenantId,
        organizationId: input.organizationId
      });
    }
    return { ok: true, id: message.id };
  },
  async captureAfter(rawInput, _result, ctx) {
    const input = updateDraftCommandSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    return loadMessageAggregateSnapshot(em, input.messageId, {
      tenantId: input.tenantId,
      organizationId: input.organizationId
    });
  },
  buildLog: async ({ input, snapshots }) => {
    const parsed = updateDraftCommandSchema.parse(input);
    return {
      actionLabel: parsed.isDraft === false ? "Send draft message" : "Update draft message",
      resourceKind: "messages.message",
      resourceId: parsed.messageId,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      payload: {
        undo: {
          before: snapshots.before ?? null,
          after: snapshots.after ?? null
        }
      },
      snapshotBefore: snapshots.before ?? null,
      snapshotAfter: snapshots.after ?? null
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const before = undo?.before;
    if (!before) return;
    const em = ctx.container.resolve("em").fork();
    await restoreMessageAggregateSnapshot(em, before);
  }
};
const replyMessageCommand = {
  id: "messages.messages.reply",
  async execute(rawInput, ctx) {
    const input = replyCommandSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const original = await requireMessageById(em, input, input.messageId);
    const ownRecipient = await em.findOne(MessageRecipient, {
      messageId: original.id,
      recipientUserId: input.userId,
      deletedAt: null
    });
    if (original.senderUserId !== input.userId && !ownRecipient) throw new Error("Access denied");
    const messageType = getMessageTypeOrDefault(original.type);
    if (messageType.allowReply === false) throw new Error("Reply is not allowed for this message type");
    const recipientIds = new Set(
      (input.recipients ?? []).map((recipient) => recipient.userId).filter((recipientUserId) => recipientUserId !== input.userId)
    );
    if (recipientIds.size === 0) {
      const originalRecipients = await em.find(MessageRecipient, { messageId: original.id, deletedAt: null });
      if (input.replyAll) {
        if (original.senderUserId !== input.userId) recipientIds.add(original.senderUserId);
        for (const recipient of originalRecipients) {
          if (recipient.recipientUserId !== input.userId) recipientIds.add(recipient.recipientUserId);
        }
      } else if (original.senderUserId !== input.userId) {
        recipientIds.add(original.senderUserId);
      } else {
        for (const recipient of originalRecipients) {
          if (recipient.recipientUserId !== input.userId) {
            recipientIds.add(recipient.recipientUserId);
            break;
          }
        }
      }
      if (recipientIds.size === 0 && original.senderUserId === input.userId) {
        recipientIds.add(input.userId);
      }
    }
    if (recipientIds.size === 0) throw new Error("No recipients available for reply");
    let messageId = "";
    let responseExternalEmail = null;
    await em.transactional(async (trx) => {
      const message = trx.create(Message, {
        type: original.type,
        visibility: original.visibility ?? null,
        sourceEntityType: original.sourceEntityType,
        sourceEntityId: original.sourceEntityId,
        externalEmail: original.externalEmail,
        externalName: original.externalName,
        threadId: original.threadId ?? original.id,
        parentMessageId: original.id,
        senderUserId: input.userId,
        subject: buildReplySubject(original.subject),
        body: input.body,
        bodyFormat: input.bodyFormat,
        priority: original.priority,
        status: "sent",
        isDraft: false,
        sentAt: /* @__PURE__ */ new Date(),
        sendViaEmail: input.sendViaEmail,
        tenantId: input.tenantId,
        organizationId: input.organizationId
      });
      await trx.persist(message).flush();
      for (const recipientUserId of recipientIds) {
        trx.persist(trx.create(MessageRecipient, {
          messageId: message.id,
          recipientUserId,
          recipientType: "to",
          status: "unread"
        }));
      }
      await trx.flush();
      if (input.attachmentIds?.length) {
        await linkAttachmentsToMessage(
          trx,
          message.id,
          input.attachmentIds,
          input.organizationId,
          input.tenantId
        );
      }
      if (input.attachmentRecordId) {
        await linkLibraryAttachmentsToMessage(
          trx,
          message.id,
          input.attachmentRecordId,
          input.organizationId,
          input.tenantId
        );
      }
      messageId = message.id;
      responseExternalEmail = message.externalEmail ?? null;
    });
    await emitMessageSentEvent(ctx.container, {
      messageId,
      senderUserId: input.userId,
      recipientUserIds: Array.from(recipientIds),
      sendViaEmail: input.sendViaEmail,
      externalEmail: responseExternalEmail,
      replyTo: original.id,
      tenantId: input.tenantId,
      organizationId: input.organizationId
    });
    await emitMessageIndexUpsert(ctx.container, {
      messageId,
      tenantId: input.tenantId,
      organizationId: input.organizationId
    });
    return {
      id: messageId,
      externalEmail: responseExternalEmail,
      recipientUserIds: Array.from(recipientIds)
    };
  },
  async captureAfter(_input, result, ctx) {
    const em = ctx.container.resolve("em").fork();
    return loadMessageAggregateSnapshot(em, result.id);
  },
  buildLog: async ({ input, result, snapshots }) => {
    const parsed = replyCommandSchema.parse(input);
    return {
      actionLabel: "Reply to message",
      resourceKind: "messages.message",
      resourceId: result.id,
      parentResourceKind: "messages.message",
      parentResourceId: parsed.messageId,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      payload: {
        undo: {
          after: snapshots.after ?? null
        }
      },
      snapshotAfter: snapshots.after ?? null
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const after = undo?.after;
    if (!after) return;
    const em = ctx.container.resolve("em").fork();
    const message = await em.findOne(Message, { id: after.message.id });
    if (!message) return;
    message.deletedAt = /* @__PURE__ */ new Date();
    await em.flush();
    await emitMessageGloballyDeletedEvent(ctx.container, {
      messageId: after.message.id,
      actorUserId: after.message.senderUserId,
      recipientUserIds: after.recipients.map((recipient) => recipient.recipientUserId),
      tenantId: after.message.tenantId,
      organizationId: after.message.organizationId
    });
  }
};
const forwardMessageCommand = {
  id: "messages.messages.forward",
  async execute(rawInput, ctx) {
    const input = forwardCommandSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const original = await requireMessageById(em, input, input.messageId);
    const isRecipient = await em.findOne(MessageRecipient, {
      messageId: input.messageId,
      recipientUserId: input.userId,
      deletedAt: null
    });
    if (original.senderUserId !== input.userId && !isRecipient) throw new Error("Access denied");
    const messageType = getMessageTypeOrDefault(original.type);
    if (messageType.allowForward === false) throw new Error("Forward is not allowed for this message type");
    const originalObjects = await em.find(MessageObject, { messageId: input.messageId });
    const forwardThreadSlice = await buildForwardThreadSlice(em, {
      tenantId: input.tenantId,
      organizationId: input.organizationId,
      userId: input.userId
    }, original);
    const generatedPreview = await buildForwardPreviewFromThreadSlice(em, {
      tenantId: input.tenantId,
      organizationId: input.organizationId,
      userId: input.userId
    }, original, forwardThreadSlice);
    const forwardedBody = typeof input.body === "string" ? input.body : buildForwardBodyFromLegacyInput(generatedPreview.body, input.additionalBody);
    let newMessageId = "";
    let responseExternalEmail = null;
    await em.transactional(async (trx) => {
      const newMessage = trx.create(Message, {
        type: original.type,
        visibility: original.visibility ?? null,
        sourceEntityType: original.sourceEntityType,
        sourceEntityId: original.sourceEntityId,
        externalEmail: original.externalEmail,
        externalName: original.externalName,
        threadId: original.threadId ?? original.id,
        parentMessageId: original.id,
        senderUserId: input.userId,
        subject: `Fwd: ${original.subject}`,
        body: forwardedBody,
        bodyFormat: original.bodyFormat,
        priority: original.priority,
        status: "sent",
        isDraft: false,
        sentAt: /* @__PURE__ */ new Date(),
        sendViaEmail: input.sendViaEmail,
        tenantId: input.tenantId,
        organizationId: input.organizationId
      });
      await trx.persist(newMessage).flush();
      for (const recipient of input.recipients) {
        trx.persist(trx.create(MessageRecipient, {
          messageId: newMessage.id,
          recipientUserId: recipient.userId,
          recipientType: recipient.type,
          status: "unread"
        }));
      }
      for (const obj of originalObjects) {
        trx.persist(trx.create(MessageObject, {
          messageId: newMessage.id,
          entityModule: obj.entityModule,
          entityType: obj.entityType,
          entityId: obj.entityId,
          actionRequired: obj.actionRequired,
          actionType: obj.actionType,
          actionLabel: obj.actionLabel,
          entitySnapshot: obj.entitySnapshot
        }));
      }
      await trx.flush();
      if (input.includeAttachments !== false) {
        await copyAttachmentsForForwardMessages(
          trx,
          forwardThreadSlice.map((item) => item.id),
          newMessage.id,
          input.tenantId
        );
      }
      newMessageId = newMessage.id;
      responseExternalEmail = newMessage.externalEmail ?? null;
    });
    await emitMessageSentEvent(ctx.container, {
      messageId: newMessageId,
      senderUserId: input.userId,
      recipientUserIds: input.recipients.map((item) => item.userId),
      sendViaEmail: input.sendViaEmail,
      externalEmail: responseExternalEmail,
      forwardedFrom: original.id,
      tenantId: input.tenantId,
      organizationId: input.organizationId
    });
    await emitMessageIndexUpsert(ctx.container, {
      messageId: newMessageId,
      tenantId: input.tenantId,
      organizationId: input.organizationId
    });
    return {
      id: newMessageId,
      externalEmail: responseExternalEmail,
      recipientUserIds: input.recipients.map((item) => item.userId)
    };
  },
  async captureAfter(_input, result, ctx) {
    const em = ctx.container.resolve("em").fork();
    return loadMessageAggregateSnapshot(em, result.id);
  },
  buildLog: async ({ input, result, snapshots }) => {
    const parsed = forwardCommandSchema.parse(input);
    return {
      actionLabel: "Forward message",
      resourceKind: "messages.message",
      resourceId: result.id,
      parentResourceKind: "messages.message",
      parentResourceId: parsed.messageId,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      payload: {
        undo: {
          after: snapshots.after ?? null
        }
      },
      snapshotAfter: snapshots.after ?? null
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const after = undo?.after;
    if (!after) return;
    const em = ctx.container.resolve("em").fork();
    const message = await em.findOne(Message, { id: after.message.id });
    if (!message) return;
    message.deletedAt = /* @__PURE__ */ new Date();
    await em.flush();
    await emitMessageGloballyDeletedEvent(ctx.container, {
      messageId: after.message.id,
      actorUserId: after.message.senderUserId,
      recipientUserIds: after.recipients.map((recipient) => recipient.recipientUserId),
      tenantId: after.message.tenantId,
      organizationId: after.message.organizationId
    });
  }
};
const deleteForActorCommand = {
  id: "messages.messages.delete_for_actor",
  async prepare(rawInput, ctx) {
    const input = deleteForActorCommandSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const message = await requireMessageById(em, input, input.messageId);
    const recipient = await em.findOne(MessageRecipient, {
      messageId: input.messageId,
      recipientUserId: input.userId,
      deletedAt: null
    });
    return {
      before: {
        messageId: message.id,
        messageDeletedAt: toIso(message.deletedAt),
        recipientId: recipient?.id ?? null,
        recipientStatus: recipient?.status ?? null,
        recipientDeletedAt: toIso(recipient?.deletedAt)
      }
    };
  },
  async execute(rawInput, ctx) {
    const input = deleteForActorCommandSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const message = await requireMessageById(em, input, input.messageId);
    const recipient = await em.findOne(MessageRecipient, {
      messageId: input.messageId,
      recipientUserId: input.userId,
      deletedAt: null
    });
    if (recipient) {
      recipient.status = "deleted";
      recipient.deletedAt = /* @__PURE__ */ new Date();
      await em.flush();
      await emitMessageDeletedEvent(ctx.container, {
        messageId: input.messageId,
        actorUserId: input.userId,
        target: "recipient",
        tenantId: input.tenantId,
        organizationId: input.organizationId
      });
      return { ok: true };
    }
    if (message.senderUserId === input.userId) {
      const recipientRows = await em.find(MessageRecipient, { messageId: input.messageId });
      message.deletedAt = /* @__PURE__ */ new Date();
      await em.flush();
      const recipientUserIds = recipientRows.map((row) => row.recipientUserId).filter((value) => typeof value === "string" && value.length > 0);
      await emitMessageGloballyDeletedEvent(ctx.container, {
        messageId: input.messageId,
        actorUserId: input.userId,
        recipientUserIds,
        tenantId: input.tenantId,
        organizationId: input.organizationId
      });
      return { ok: true };
    }
    throw new Error("Access denied");
  },
  async captureAfter(rawInput, _result, ctx) {
    const input = deleteForActorCommandSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const message = await em.findOne(Message, { id: input.messageId, tenantId: input.tenantId });
    const recipient = await em.findOne(MessageRecipient, {
      messageId: input.messageId,
      recipientUserId: input.userId
    });
    return {
      messageId: input.messageId,
      messageDeletedAt: toIso(message?.deletedAt),
      recipientId: recipient?.id ?? null,
      recipientStatus: recipient?.status ?? null,
      recipientDeletedAt: toIso(recipient?.deletedAt)
    };
  },
  buildLog: async ({ input, snapshots }) => {
    const parsed = deleteForActorCommandSchema.parse(input);
    return {
      actionLabel: "Delete message for actor",
      resourceKind: "messages.message",
      resourceId: parsed.messageId,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      payload: {
        undo: {
          before: snapshots.before ?? null,
          after: snapshots.after ?? null
        }
      },
      snapshotBefore: snapshots.before ?? null,
      snapshotAfter: snapshots.after ?? null
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const before = undo?.before;
    if (!before) return;
    const em = ctx.container.resolve("em").fork();
    const message = await em.findOne(Message, { id: before.messageId });
    if (message) {
      message.deletedAt = toDate(before.messageDeletedAt);
    }
    if (before.recipientId) {
      const recipient = await em.findOne(MessageRecipient, { id: before.recipientId });
      if (recipient) {
        recipient.status = before.recipientStatus ?? "unread";
        recipient.deletedAt = toDate(before.recipientDeletedAt);
      }
    }
    await em.flush();
  }
};
registerCommand(composeMessageCommand);
registerCommand(updateDraftCommand);
registerCommand(replyMessageCommand);
registerCommand(forwardMessageCommand);
registerCommand(deleteForActorCommand);
//# sourceMappingURL=messages.js.map
