import { z } from "zod";
import { registerCommand } from "@open-mercato/shared/lib/commands";
import { extractUndoPayload as extractSharedUndoPayload } from "@open-mercato/shared/lib/commands/undo";
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { ChannelThreadMapping, ExternalConversation } from "../data/entities.js";
import { emitCommunicationChannelsEvent } from "../events.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("communication_channels").child({ component: "reassign-conversation" });
const reassignConversationSchema = z.object({
  threadId: z.string().uuid(),
  /** Set to `null` to unassign the conversation. */
  assignedUserId: z.string().uuid().nullable(),
  scope: z.object({
    tenantId: z.string().uuid(),
    organizationId: z.string().uuid().nullable()
  })
});
const COMMUNICATION_CHANNELS_REASSIGN_CONVERSATION_COMMAND_ID = "communication_channels.conversation.reassign";
const reassignConversationCommand = {
  id: COMMUNICATION_CHANNELS_REASSIGN_CONVERSATION_COMMAND_ID,
  // Explicitly undoable (the bus also infers this from `undo` below, but
  // declaring it keeps undoability from silently dropping under a refactor).
  isUndoable: true,
  async execute(rawInput, ctx) {
    const input = reassignConversationSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const dscope = {
      tenantId: input.scope.tenantId,
      organizationId: input.scope.organizationId ?? null
    };
    const mapping = await findOneWithDecryption(
      em,
      ChannelThreadMapping,
      {
        messageThreadId: input.threadId,
        tenantId: input.scope.tenantId,
        organizationId: input.scope.organizationId ?? null
      },
      // `message_thread_id` is non-unique (only `(external_conversation_id,
      // tenant_id)` is unique). The 1:1 thread↔mapping invariant holds in v1,
      // but order deterministically so a future many-conversations-per-thread
      // merge can never resolve an arbitrary mapping here.
      { orderBy: { createdAt: "asc" } },
      dscope
    );
    if (!mapping) {
      return {
        status: "no_channel_link",
        reason: `no ChannelThreadMapping for thread ${input.threadId}`
      };
    }
    const previousAssignedUserId = mapping.assignedUserId ?? null;
    if (previousAssignedUserId === input.assignedUserId) {
      return { status: "noop", reason: "assigned user unchanged" };
    }
    if (input.assignedUserId) {
      const assignee = await findOneWithDecryption(
        em,
        "User",
        { id: input.assignedUserId, tenantId: input.scope.tenantId, deletedAt: null },
        void 0,
        dscope
      );
      if (!assignee) {
        return {
          status: "invalid_assignee",
          reason: "assigned user is not a member of this tenant"
        };
      }
    }
    const conversation = await findOneWithDecryption(
      em,
      ExternalConversation,
      {
        id: mapping.externalConversationId,
        tenantId: input.scope.tenantId,
        organizationId: input.scope.organizationId ?? null
      },
      void 0,
      dscope
    );
    if (!conversation) {
      return {
        status: "no_channel_link",
        reason: `no ExternalConversation for thread ${input.threadId}`
      };
    }
    mapping.assignedUserId = input.assignedUserId;
    conversation.assignedUserId = input.assignedUserId;
    await em.flush();
    try {
      await emitCommunicationChannelsEvent(
        "communication_channels.conversation.reassigned",
        {
          conversationId: conversation.id,
          channelId: mapping.channelId,
          messageThreadId: input.threadId,
          previousAssignedUserId,
          assignedUserId: input.assignedUserId,
          tenantId: input.scope.tenantId,
          organizationId: input.scope.organizationId ?? null
        },
        { persistent: true }
      );
    } catch (emitErr) {
      logger.warn(
        "reassigned event emit failed",
        { err: emitErr }
      );
    }
    const undo = {
      threadMappingId: mapping.id,
      conversationId: conversation.id,
      tenantId: mapping.tenantId,
      previousAssignedUserId,
      newAssignedUserId: input.assignedUserId
    };
    return {
      status: "reassigned",
      threadId: input.threadId,
      previousAssignedUserId,
      nextAssignedUserId: input.assignedUserId,
      conversationId: conversation.id,
      undo
    };
  },
  // Persist the undo snapshot into the action log. Without this, the command bus
  // mints an undo token (so the UI offers "Undo") but the snapshot returned from
  // execute() is never stored, and undo() would silently no-op.
  async buildLog({ input, result }) {
    if (result.status !== "reassigned") return null;
    return {
      resourceKind: "communication_channels.channel",
      resourceId: result.conversationId,
      tenantId: result.undo.tenantId ?? input.scope.tenantId,
      organizationId: input.scope.organizationId ?? null,
      payload: { undo: result.undo },
      snapshotBefore: result.undo
    };
  },
  async undo({ ctx, logEntry }) {
    const snapshot = extractSnapshotFromLog(logEntry);
    if (!snapshot) return;
    const em = ctx.container.resolve("em").fork();
    if (!snapshot.tenantId) return;
    const dscope = { tenantId: snapshot.tenantId, organizationId: null };
    const mapping = await findOneWithDecryption(
      em,
      ChannelThreadMapping,
      { id: snapshot.threadMappingId, tenantId: snapshot.tenantId },
      void 0,
      dscope
    );
    const conversation = await findOneWithDecryption(
      em,
      ExternalConversation,
      { id: snapshot.conversationId, tenantId: snapshot.tenantId },
      void 0,
      dscope
    );
    if (mapping) mapping.assignedUserId = snapshot.previousAssignedUserId;
    if (conversation) conversation.assignedUserId = snapshot.previousAssignedUserId;
    if (mapping || conversation) await em.flush();
  }
};
function extractUndoPayload(value) {
  if (!value || typeof value !== "object") return null;
  const candidate = value.undo ?? value;
  if (!candidate || typeof candidate !== "object") return null;
  const obj = candidate;
  if (typeof obj.threadMappingId !== "string" || typeof obj.conversationId !== "string") return null;
  return {
    threadMappingId: obj.threadMappingId,
    conversationId: obj.conversationId,
    tenantId: typeof obj.tenantId === "string" ? obj.tenantId : void 0,
    previousAssignedUserId: typeof obj.previousAssignedUserId === "string" ? obj.previousAssignedUserId : null,
    newAssignedUserId: typeof obj.newAssignedUserId === "string" ? obj.newAssignedUserId : null
  };
}
function extractSnapshotFromLog(logEntry) {
  const undo = extractSharedUndoPayload(
    logEntry ?? null
  );
  if (undo) return extractUndoPayload(undo);
  return extractUndoPayload(logEntry);
}
registerCommand(reassignConversationCommand);
var reassign_conversation_default = reassignConversationCommand;
export {
  COMMUNICATION_CHANNELS_REASSIGN_CONVERSATION_COMMAND_ID,
  reassign_conversation_default as default,
  extractSnapshotFromLog,
  extractUndoPayload
};
//# sourceMappingURL=reassign-conversation.js.map
