import { registerCommand } from "@open-mercato/shared/lib/commands";
import {
  parseWithCustomFields,
  setCustomFieldsIfAny,
  emitCrudSideEffects,
  emitCrudUndoSideEffects,
  buildChanges,
  requireId
} from "@open-mercato/shared/lib/commands/helpers";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import { UniqueConstraintViolationException } from "@mikro-orm/core";
import { User, UserRole, Role, UserAcl, Session, PasswordReset } from "@open-mercato/core/modules/auth/data/entities";
import { Organization } from "@open-mercato/core/modules/directory/data/entities";
import { E } from "../../../generated/entities.ids.generated.js";
import { z } from "zod";
import {
  loadCustomFieldSnapshot,
  buildCustomFieldResetMap,
  diffCustomFieldChanges
} from "@open-mercato/shared/lib/commands/customFieldSnapshots";
import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
import { resolveRedoSnapshot } from "@open-mercato/shared/lib/commands/redo";
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
import { normalizeTenantId } from "@open-mercato/core/modules/auth/lib/tenantAccess";
import { computeEmailHash, emailHashLookupValues } from "@open-mercato/core/modules/auth/lib/emailHash";
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { buildNotificationFromType } from "@open-mercato/core/modules/notifications/lib/notificationBuilder";
import { resolveNotificationService } from "@open-mercato/core/modules/notifications/lib/notificationService";
import notificationTypes from "@open-mercato/core/modules/auth/notifications";
import { buildPasswordSchema } from "@open-mercato/shared/lib/auth/passwordPolicy";
import { sendEmail } from "@open-mercato/shared/lib/email/send";
import InviteUserEmail from "@open-mercato/core/modules/auth/emails/InviteUserEmail";
import { INVITE_TOKEN_TTL_MS } from "@open-mercato/core/modules/auth/lib/inviteToken";
import { getSecurityEmailBaseUrl } from "@open-mercato/shared/lib/url";
import { generateAuthToken, hashAuthToken } from "@open-mercato/core/modules/auth/lib/tokenHash";
import { normalizeDisplayNameInput } from "@open-mercato/core/modules/auth/lib/displayName";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("auth").child({ component: "users-commands" });
function resolveActorTenantScope(ctx) {
  if (ctx.systemActor === true) return null;
  const auth = ctx.auth;
  if (!auth) return null;
  if (auth.isSuperAdmin === true) return null;
  const actorTenantId = normalizeTenantId(auth.tenantId ?? null) ?? null;
  return actorTenantId;
}
function assertTargetTenantInScope(actorTenantScope, targetTenantId, notFoundError) {
  if (!actorTenantScope) return;
  const targetTenant = normalizeTenantId(targetTenantId) ?? null;
  if (!targetTenant || targetTenant !== actorTenantScope) {
    throw new CrudHttpError(404, { error: notFoundError });
  }
}
const passwordSchema = buildPasswordSchema();
const displayNameSchema = z.preprocess(
  normalizeDisplayNameInput,
  z.string().trim().min(1).max(120).nullable().optional()
);
const createSchema = z.object({
  email: z.string().email(),
  name: displayNameSchema,
  password: passwordSchema.optional(),
  sendInviteEmail: z.boolean().optional(),
  organizationId: z.string().uuid(),
  roles: z.array(z.string()).optional()
}).refine(
  (data) => data.password || data.sendInviteEmail,
  { message: "Either password or sendInviteEmail is required", path: ["password"] }
);
const updateSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email().optional(),
  name: displayNameSchema,
  password: passwordSchema.optional(),
  organizationId: z.string().uuid().optional(),
  roles: z.array(z.string()).optional()
});
const userCrudEvents = {
  module: "auth",
  entity: "user",
  persistent: true,
  buildPayload: (ctx) => ({
    id: ctx.identifiers.id,
    organizationId: ctx.identifiers.organizationId,
    tenantId: ctx.identifiers.tenantId
  })
};
const userCrudIndexer = {
  entityType: E.auth.user,
  buildUpsertPayload: (ctx) => ({
    entityType: E.auth.user,
    recordId: ctx.identifiers.id,
    organizationId: ctx.identifiers.organizationId,
    tenantId: ctx.identifiers.tenantId
  }),
  buildDeletePayload: (ctx) => ({
    entityType: E.auth.user,
    recordId: ctx.identifiers.id,
    organizationId: ctx.identifiers.organizationId,
    tenantId: ctx.identifiers.tenantId
  })
};
async function notifyRoleChanges(ctx, user, assignedRoles, revokedRoles) {
  const tenantId = user.tenantId ? String(user.tenantId) : null;
  if (!tenantId) return;
  const organizationId = user.organizationId ? String(user.organizationId) : null;
  try {
    const notificationService = resolveNotificationService(ctx.container);
    if (assignedRoles.length) {
      const assignedType = notificationTypes.find((type) => type.type === "auth.role.assigned");
      if (assignedType) {
        const notificationInput = buildNotificationFromType(assignedType, {
          recipientUserId: String(user.id),
          sourceEntityType: "auth:user",
          sourceEntityId: String(user.id)
        });
        await notificationService.create(notificationInput, { tenantId, organizationId });
      }
    }
    if (revokedRoles.length) {
      const revokedType = notificationTypes.find((type) => type.type === "auth.role.revoked");
      if (revokedType) {
        const notificationInput = buildNotificationFromType(revokedType, {
          recipientUserId: String(user.id),
          sourceEntityType: "auth:user",
          sourceEntityId: String(user.id)
        });
        await notificationService.create(notificationInput, { tenantId, organizationId });
      }
    }
  } catch (err) {
    logger.error("Failed to create notification", { err });
  }
}
const createUserCommand = {
  id: "auth.users.create",
  async execute(rawInput, ctx) {
    const { parsed, custom } = parseWithCustomFields(createSchema, rawInput);
    const em = ctx.container.resolve("em");
    const organization = await findOneWithDecryption(
      em,
      Organization,
      { id: parsed.organizationId },
      { populate: ["tenant"] },
      { tenantId: null, organizationId: parsed.organizationId }
    );
    if (!organization) throw new CrudHttpError(400, { error: "Organization not found" });
    const tenantId = organization.tenant?.id ? String(organization.tenant.id) : null;
    assertTargetTenantInScope(resolveActorTenantScope(ctx), tenantId, "Organization not found");
    const emailHash = computeEmailHash(parsed.email);
    const duplicate = await findOneWithDecryption(em, User, { $or: [{ email: parsed.email }, { emailHash: { $in: emailHashLookupValues(parsed.email) } }], deletedAt: null, tenantId }, {}, { tenantId: null, organizationId: null });
    if (duplicate) await throwDuplicateEmailError();
    let passwordHash = null;
    if (parsed.password) {
      const { hash } = await import("bcryptjs");
      passwordHash = await hash(parsed.password, 10);
    }
    const de = ctx.container.resolve("dataEngine");
    let user;
    try {
      user = await de.createOrmEntity({
        entity: User,
        data: {
          email: parsed.email,
          name: parsed.name,
          emailHash,
          passwordHash,
          isConfirmed: true,
          organizationId: parsed.organizationId,
          tenantId
        }
      });
    } catch (error) {
      if (isUniqueViolation(error)) await throwDuplicateEmailError();
      throw error;
    }
    let assignedRoles = [];
    if (Array.isArray(parsed.roles) && parsed.roles.length) {
      await syncUserRoles(em, user, parsed.roles, tenantId);
      assignedRoles = await loadUserRoleNames(em, String(user.id));
    }
    await setCustomFieldsIfAny({
      dataEngine: de,
      entityId: E.auth.user,
      recordId: String(user.id),
      organizationId: user.organizationId ? String(user.organizationId) : null,
      tenantId,
      values: custom
    });
    let inviteEmailSent = false;
    if (parsed.sendInviteEmail) {
      const inviteResult = await sendInviteToUser(em, user);
      inviteEmailSent = inviteResult.emailSent;
    }
    await emitCrudSideEffects({
      dataEngine: de,
      action: "created",
      entity: user,
      identifiers: {
        id: String(user.id),
        organizationId: user.organizationId ? String(user.organizationId) : null,
        tenantId
      },
      events: userCrudEvents,
      indexer: userCrudIndexer
    });
    if (assignedRoles.length && !parsed.sendInviteEmail) {
      await notifyRoleChanges(ctx, user, assignedRoles, []);
    }
    const warning = parsed.sendInviteEmail && !inviteEmailSent ? "invite_email_failed" : void 0;
    return { user, warning };
  },
  captureAfter: async (_input, { user }, ctx) => {
    const em = ctx.container.resolve("em").fork();
    const roles = await loadUserRoleNames(em, String(user.id));
    const custom = await loadUserCustomSnapshot(
      em,
      String(user.id),
      user.tenantId ? String(user.tenantId) : null,
      user.organizationId ? String(user.organizationId) : null
    );
    return serializeUser(user, roles, custom);
  },
  buildLog: async ({ result: { user }, ctx }) => {
    const { translate } = await resolveTranslations();
    const em = ctx.container.resolve("em").fork();
    const roles = await loadUserRoleNames(em, String(user.id));
    const custom = await loadUserCustomSnapshot(
      em,
      String(user.id),
      user.tenantId ? String(user.tenantId) : null,
      user.organizationId ? String(user.organizationId) : null
    );
    const snapshot = captureUserSnapshots(user, roles, void 0, custom);
    return {
      actionLabel: translate("auth.audit.users.create", "Create user"),
      resourceKind: "auth.user",
      resourceId: String(user.id),
      tenantId: user.tenantId ? String(user.tenantId) : null,
      organizationId: user.organizationId ? String(user.organizationId) : null,
      snapshotAfter: snapshot.view,
      payload: {
        undo: {
          after: snapshot.undo
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const userId = typeof logEntry?.resourceId === "string" ? logEntry.resourceId : null;
    if (!userId) return;
    const snapshot = logEntry?.snapshotAfter;
    const em = ctx.container.resolve("em");
    const de = ctx.container.resolve("dataEngine");
    let removed = null;
    await withAtomicFlush(em, [
      async () => {
        await em.nativeDelete(UserAcl, { user: userId });
        await em.nativeDelete(UserRole, { user: userId });
        await em.nativeDelete(Session, { user: userId });
        await em.nativeDelete(PasswordReset, { user: userId });
        if (snapshot?.custom && Object.keys(snapshot.custom).length) {
          const reset = buildCustomFieldResetMap(void 0, snapshot.custom);
          if (Object.keys(reset).length) {
            await setCustomFieldsIfAny({
              dataEngine: de,
              entityId: E.auth.user,
              recordId: userId,
              organizationId: snapshot.organizationId,
              tenantId: snapshot.tenantId,
              values: reset,
              notify: false
            });
          }
        }
        removed = await de.deleteOrmEntity({
          entity: User,
          where: { id: userId, deletedAt: null },
          soft: false
        });
      }
    ], { transaction: true });
    await emitCrudUndoSideEffects({
      dataEngine: de,
      action: "deleted",
      entity: removed,
      identifiers: {
        id: userId,
        organizationId: snapshot?.organizationId ?? null,
        tenantId: snapshot?.tenantId ?? null
      },
      events: userCrudEvents,
      indexer: userCrudIndexer
    });
    await invalidateUserCache(ctx, userId);
  },
  // The create-undo hard-deletes the user, but the after-snapshot persists the
  // original passwordHash (see captureUserSnapshots), so redo restores the row
  // with the SAME id and the SAME hash — never fabricating credentials (#2506).
  redo: async ({ logEntry, ctx }) => {
    const after = resolveRedoSnapshot(logEntry);
    if (!after) throw new CrudHttpError(400, { error: "[internal] redo snapshot unavailable for user create" });
    const em = ctx.container.resolve("em");
    const de = ctx.container.resolve("dataEngine");
    const emailHash = computeEmailHash(after.email);
    let user = await findOneWithDecryption(em, User, { id: after.id }, {}, { tenantId: null, organizationId: null });
    await withAtomicFlush(em, [
      async () => {
        if (user) {
          user.deletedAt = null;
          user.email = after.email;
          user.emailHash = emailHash;
          user.organizationId = after.organizationId ?? null;
          user.tenantId = after.tenantId ?? null;
          user.passwordHash = after.passwordHash ?? null;
          user.name = after.name ?? null;
          user.isConfirmed = after.isConfirmed;
          await em.flush();
        } else {
          user = await de.createOrmEntity({
            entity: User,
            data: {
              id: after.id,
              email: after.email,
              emailHash,
              organizationId: after.organizationId ?? null,
              tenantId: after.tenantId ?? null,
              passwordHash: after.passwordHash ?? null,
              name: after.name ?? null,
              isConfirmed: after.isConfirmed
            }
          });
        }
        if (!user) return;
        await em.nativeDelete(UserRole, { user: after.id });
        await syncUserRoles(em, user, after.roles, after.tenantId);
        await restoreUserAcls(em, user, after.acls);
        if (after.custom && Object.keys(after.custom).length) {
          const reset = buildCustomFieldResetMap(after.custom, void 0);
          if (Object.keys(reset).length) {
            await setCustomFieldsIfAny({
              dataEngine: de,
              entityId: E.auth.user,
              recordId: after.id,
              organizationId: after.organizationId ?? null,
              tenantId: after.tenantId ?? null,
              values: reset,
              notify: false
            });
          }
        }
      }
    ], { transaction: true });
    if (!user) throw new CrudHttpError(400, { error: "[internal] redo failed to restore user row" });
    await emitCrudSideEffects({
      dataEngine: de,
      action: "created",
      entity: user,
      identifiers: {
        id: after.id,
        organizationId: after.organizationId ?? null,
        tenantId: after.tenantId ?? null
      },
      events: userCrudEvents,
      indexer: userCrudIndexer
    });
    await invalidateUserCache(ctx, after.id);
    return { user };
  }
};
async function sendInviteToUser(em, user) {
  const rawToken = generateAuthToken();
  const tokenHash = hashAuthToken(rawToken);
  const expiresAt = new Date(Date.now() + INVITE_TOKEN_TTL_MS);
  const row = em.create(PasswordReset, { user, token: tokenHash, expiresAt, createdAt: /* @__PURE__ */ new Date() });
  await em.persist(row).flush();
  const base = getSecurityEmailBaseUrl();
  const inviteUrl = `${base}/reset/${rawToken}`;
  const { translate } = await resolveTranslations();
  const subject = translate("auth.email.invite.subject", "You have been invited");
  const copy = {
    preview: translate("auth.email.invite.preview", "Set up your account"),
    title: translate("auth.email.invite.title", "You have been invited"),
    body: translate("auth.email.invite.body", "An administrator has created an account for you. Click the link below to set your password. This link will expire in 48 hours."),
    cta: translate("auth.email.invite.cta", "Set up your password"),
    hint: translate("auth.email.invite.hint", "If you did not expect this invitation, you can safely ignore this email.")
  };
  let emailSent = true;
  try {
    await sendEmail({ to: user.email, subject, react: InviteUserEmail({ inviteUrl, copy }) });
  } catch (err) {
    logger.error("Failed to send invitation email", { err });
    emailSent = false;
  }
  return { emailSent };
}
function isUniqueViolation(error) {
  if (error instanceof UniqueConstraintViolationException) return true;
  if (!error || typeof error !== "object") return false;
  const code = error.code;
  if (code === "23505") return true;
  const messageRaw = error?.message;
  const message = typeof messageRaw === "string" ? messageRaw : "";
  return message.toLowerCase().includes("duplicate key");
}
const updateUserCommand = {
  id: "auth.users.update",
  async prepare(rawInput, ctx) {
    const { parsed } = parseWithCustomFields(updateSchema, rawInput);
    const em = ctx.container.resolve("em");
    const existing = await findOneWithDecryption(em, User, { id: parsed.id, deletedAt: null }, {}, { tenantId: null, organizationId: null });
    if (!existing) throw new CrudHttpError(404, { error: "User not found" });
    assertTargetTenantInScope(resolveActorTenantScope(ctx), existing.tenantId, "User not found");
    const roles = await loadUserRoleNames(em, parsed.id);
    const acls = await loadUserAclSnapshots(em, parsed.id);
    const custom = await loadUserCustomSnapshot(
      em,
      parsed.id,
      existing.tenantId ? String(existing.tenantId) : null,
      existing.organizationId ? String(existing.organizationId) : null
    );
    return { before: captureUserSnapshots(existing, roles, acls, custom) };
  },
  async execute(rawInput, ctx) {
    const { parsed, custom } = parseWithCustomFields(updateSchema, rawInput);
    const em = ctx.container.resolve("em");
    const rolesBefore = Array.isArray(parsed.roles) ? await loadUserRoleNames(em, parsed.id) : null;
    let tenantId;
    if (parsed.organizationId !== void 0) {
      const organization = await findOneWithDecryption(
        em,
        Organization,
        { id: parsed.organizationId },
        { populate: ["tenant"] },
        { tenantId: null, organizationId: parsed.organizationId ?? null }
      );
      if (!organization) throw new CrudHttpError(400, { error: "Organization not found" });
      tenantId = organization.tenant?.id ? String(organization.tenant.id) : null;
    }
    if (parsed.email !== void 0) {
      const targetTenantId = tenantId !== void 0 ? tenantId : await resolveUserTenantId(em, parsed.id);
      const duplicate = await findOneWithDecryption(
        em,
        User,
        {
          $or: [{ email: parsed.email }, { emailHash: { $in: emailHashLookupValues(parsed.email) } }],
          deletedAt: null,
          tenantId: targetTenantId,
          id: { $ne: parsed.id }
        },
        {},
        { tenantId: null, organizationId: null }
      );
      if (duplicate) await throwDuplicateEmailError();
    }
    let hashed = null;
    let emailHash = null;
    if (parsed.password) {
      const { hash } = await import("bcryptjs");
      hashed = await hash(parsed.password, 10);
    }
    if (parsed.email !== void 0) {
      emailHash = computeEmailHash(parsed.email);
    }
    const actorTenantScope = resolveActorTenantScope(ctx);
    const updateWhere = { id: parsed.id, deletedAt: null };
    if (actorTenantScope) updateWhere.tenantId = actorTenantScope;
    const de = ctx.container.resolve("dataEngine");
    let user;
    try {
      user = await de.updateOrmEntity({
        entity: User,
        where: updateWhere,
        apply: (entity) => {
          if (parsed.email !== void 0) {
            entity.email = parsed.email;
            entity.emailHash = emailHash;
          }
          if (parsed.name !== void 0) {
            entity.name = parsed.name;
          }
          if (parsed.organizationId !== void 0) {
            entity.organizationId = parsed.organizationId;
            entity.tenantId = tenantId ?? null;
          }
          if (hashed) entity.passwordHash = hashed;
        }
      });
    } catch (error) {
      if (isUniqueViolation(error)) await throwDuplicateEmailError();
      throw error;
    }
    if (!user) throw new CrudHttpError(404, { error: "User not found" });
    if (hashed) {
      await em.nativeDelete(Session, { user: parsed.id });
    }
    if (Array.isArray(parsed.roles)) {
      await syncUserRoles(em, user, parsed.roles, user.tenantId ? String(user.tenantId) : tenantId ?? null);
    }
    await setCustomFieldsIfAny({
      dataEngine: de,
      entityId: E.auth.user,
      recordId: String(user.id),
      organizationId: user.organizationId ? String(user.organizationId) : null,
      tenantId: user.tenantId ? String(user.tenantId) : tenantId ?? null,
      values: custom
    });
    const identifiers = {
      id: String(user.id),
      organizationId: user.organizationId ? String(user.organizationId) : null,
      tenantId: user.tenantId ? String(user.tenantId) : tenantId ?? null
    };
    await emitCrudSideEffects({
      dataEngine: de,
      action: "updated",
      entity: user,
      identifiers,
      events: userCrudEvents,
      indexer: userCrudIndexer
    });
    if (Array.isArray(parsed.roles) && rolesBefore) {
      const rolesAfter = await loadUserRoleNames(em, String(user.id));
      const { assigned, revoked } = diffRoleChanges(rolesBefore, rolesAfter);
      if (assigned.length || revoked.length) {
        await notifyRoleChanges(ctx, user, assigned, revoked);
      }
    }
    await invalidateUserCache(ctx, parsed.id);
    return user;
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    const roles = await loadUserRoleNames(em, String(result.id));
    const custom = await loadUserCustomSnapshot(
      em,
      String(result.id),
      result.tenantId ? String(result.tenantId) : null,
      result.organizationId ? String(result.organizationId) : null
    );
    return serializeUser(result, roles, custom);
  },
  buildLog: async ({ result, snapshots, ctx }) => {
    const { translate } = await resolveTranslations();
    const beforeSnapshots = snapshots.before;
    const before = beforeSnapshots?.view;
    const beforeUndo = beforeSnapshots?.undo ?? null;
    const em = ctx.container.resolve("em").fork();
    const afterRoles = await loadUserRoleNames(em, String(result.id));
    const afterCustom = await loadUserCustomSnapshot(
      em,
      String(result.id),
      result.tenantId ? String(result.tenantId) : null,
      result.organizationId ? String(result.organizationId) : null
    );
    const afterSnapshots = captureUserSnapshots(result, afterRoles, void 0, afterCustom);
    const after = afterSnapshots.view;
    const changes = buildChanges(before ?? null, after, ["email", "organizationId", "tenantId", "name", "isConfirmed"]);
    if (before && !arrayEquals(before.roles, afterRoles)) {
      changes.roles = { from: before.roles, to: afterRoles };
    }
    const customDiff = diffCustomFieldChanges(before?.custom, afterCustom);
    for (const [key, diff] of Object.entries(customDiff)) {
      changes[`cf_${key}`] = diff;
    }
    return {
      actionLabel: translate("auth.audit.users.update", "Update user"),
      resourceKind: "auth.user",
      resourceId: String(result.id),
      tenantId: result.tenantId ? String(result.tenantId) : null,
      organizationId: result.organizationId ? String(result.organizationId) : null,
      changes,
      snapshotBefore: before ?? null,
      snapshotAfter: after,
      payload: {
        undo: {
          before: beforeUndo,
          after: afterSnapshots.undo
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const before = payload?.before;
    const after = payload?.after;
    if (!before) return;
    const userId = before.id;
    const em = ctx.container.resolve("em");
    const de = ctx.container.resolve("dataEngine");
    const updated = await de.updateOrmEntity({
      entity: User,
      where: { id: userId, deletedAt: null },
      apply: (entity) => {
        entity.email = before.email;
        entity.organizationId = before.organizationId ?? null;
        entity.tenantId = before.tenantId ?? null;
        entity.passwordHash = before.passwordHash ?? null;
        entity.name = before.name ?? null;
        entity.isConfirmed = before.isConfirmed;
      }
    });
    if (updated) {
      await syncUserRoles(em, updated, before.roles, before.tenantId);
      await em.flush();
    }
    const reset = buildCustomFieldResetMap(before.custom, after?.custom);
    if (Object.keys(reset).length) {
      await setCustomFieldsIfAny({
        dataEngine: de,
        entityId: E.auth.user,
        recordId: before.id,
        organizationId: before.organizationId ?? null,
        tenantId: before.tenantId ?? null,
        values: reset,
        notify: false
      });
    }
    await emitCrudUndoSideEffects({
      dataEngine: de,
      action: "updated",
      entity: updated,
      identifiers: {
        id: before.id,
        organizationId: before.organizationId ?? null,
        tenantId: before.tenantId ?? null
      },
      events: userCrudEvents,
      indexer: userCrudIndexer
    });
    await invalidateUserCache(ctx, userId);
  }
};
const deleteUserCommand = {
  id: "auth.users.delete",
  async prepare(input, ctx) {
    const id = requireId(input, "User id required");
    const em = ctx.container.resolve("em");
    const existing = await findOneWithDecryption(em, User, { id, deletedAt: null }, {}, { tenantId: null, organizationId: null });
    if (!existing) return {};
    const actorTenantScope = resolveActorTenantScope(ctx);
    if (actorTenantScope) {
      const targetTenant = normalizeTenantId(existing.tenantId) ?? null;
      if (!targetTenant || targetTenant !== actorTenantScope) return {};
    }
    const roles = await loadUserRoleNames(em, id);
    const acls = await loadUserAclSnapshots(em, id);
    const custom = await loadUserCustomSnapshot(
      em,
      id,
      existing.tenantId ? String(existing.tenantId) : null,
      existing.organizationId ? String(existing.organizationId) : null
    );
    return { before: captureUserSnapshots(existing, roles, acls, custom) };
  },
  async execute(input, ctx) {
    const id = requireId(input, "User id required");
    const em = ctx.container.resolve("em");
    const de = ctx.container.resolve("dataEngine");
    const actorTenantScope = resolveActorTenantScope(ctx);
    const deleteWhere = { id, deletedAt: null };
    if (actorTenantScope) deleteWhere.tenantId = actorTenantScope;
    let user;
    await withAtomicFlush(em, [
      async () => {
        await em.nativeDelete(UserAcl, { user: id });
        await em.nativeDelete(UserRole, { user: id });
        await em.nativeDelete(Session, { user: id });
        await em.nativeDelete(PasswordReset, { user: id });
        const removed = await de.deleteOrmEntity({
          entity: User,
          where: deleteWhere,
          soft: false
        });
        if (!removed) throw new CrudHttpError(404, { error: "User not found" });
        user = removed;
      }
    ], { transaction: true });
    await emitCrudSideEffects({
      dataEngine: de,
      action: "deleted",
      entity: user,
      identifiers: {
        id: String(id),
        organizationId: user.organizationId ? String(user.organizationId) : null,
        tenantId: user.tenantId ? String(user.tenantId) : null
      },
      events: userCrudEvents,
      indexer: userCrudIndexer
    });
    await invalidateUserCache(ctx, id);
    return user;
  },
  buildLog: async ({ snapshots, input, ctx }) => {
    const { translate } = await resolveTranslations();
    const beforeSnapshots = snapshots.before;
    const before = beforeSnapshots?.view;
    const beforeUndo = beforeSnapshots?.undo ?? null;
    const id = requireId(input, "User id required");
    return {
      actionLabel: translate("auth.audit.users.delete", "Delete user"),
      resourceKind: "auth.user",
      resourceId: id,
      snapshotBefore: before ?? null,
      tenantId: before?.tenantId ?? null,
      organizationId: before?.organizationId ?? null,
      payload: {
        undo: {
          before: beforeUndo
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const before = payload?.before;
    if (!before) return;
    const em = ctx.container.resolve("em");
    let user = await findOneWithDecryption(em, User, { id: before.id }, {}, { tenantId: null, organizationId: null });
    const de = ctx.container.resolve("dataEngine");
    await withAtomicFlush(em, [
      async () => {
        if (user) {
          if (user.deletedAt) {
            user.deletedAt = null;
          }
          user.email = before.email;
          user.organizationId = before.organizationId ?? null;
          user.tenantId = before.tenantId ?? null;
          user.passwordHash = before.passwordHash ?? null;
          user.name = before.name ?? null;
          user.isConfirmed = before.isConfirmed;
          await em.flush();
        } else {
          user = await de.createOrmEntity({
            entity: User,
            data: {
              id: before.id,
              email: before.email,
              organizationId: before.organizationId ?? null,
              tenantId: before.tenantId ?? null,
              passwordHash: before.passwordHash ?? null,
              name: before.name ?? null,
              isConfirmed: before.isConfirmed
            }
          });
        }
        if (!user) return;
        await em.nativeDelete(UserRole, { user: before.id });
        await syncUserRoles(em, user, before.roles, before.tenantId);
        await restoreUserAcls(em, user, before.acls);
        const reset = buildCustomFieldResetMap(before.custom, void 0);
        if (Object.keys(reset).length) {
          await setCustomFieldsIfAny({
            dataEngine: de,
            entityId: E.auth.user,
            recordId: before.id,
            organizationId: before.organizationId ?? null,
            tenantId: before.tenantId ?? null,
            values: reset,
            notify: false
          });
        }
      }
    ], { transaction: true });
    await invalidateUserCache(ctx, before.id);
  }
};
registerCommand(createUserCommand);
registerCommand(updateUserCommand);
registerCommand(deleteUserCommand);
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
async function resolveRole(em, value, normalizedTenantId) {
  if (UUID_RE.test(value)) {
    const where = { id: value };
    if (normalizedTenantId !== null) {
      where.tenantId = normalizedTenantId;
    }
    return findOneWithDecryption(em, Role, where, {}, { tenantId: normalizedTenantId, organizationId: null });
  }
  return findOneWithDecryption(em, Role, { name: value, tenantId: normalizedTenantId }, {}, { tenantId: normalizedTenantId, organizationId: null });
}
async function syncUserRoles(em, user, desiredRoles, tenantId) {
  const unique = Array.from(new Set(desiredRoles.map((role) => role.trim()).filter(Boolean)));
  const normalizedTenantId = normalizeTenantId(tenantId ?? null) ?? null;
  const resolvedRoles = [];
  const missingRoles = [];
  for (const value of unique) {
    const role = await resolveRole(em, value, normalizedTenantId);
    if (!role) {
      missingRoles.push(value);
    } else {
      resolvedRoles.push(role);
    }
  }
  if (missingRoles.length) {
    const labels = missingRoles.map((n) => `"${n}"`).join(", ");
    throw new CrudHttpError(400, { error: `Role(s) not found: ${labels}` });
  }
  const desiredIds = new Set(resolvedRoles.map((r) => String(r.id)));
  const currentLinks = await findWithDecryption(em, UserRole, { user }, {}, { tenantId: null, organizationId: null });
  const currentRoleIds = new Map(
    currentLinks.map((link) => {
      const roleId = String(link.role?.id ?? link.role ?? "");
      return [roleId, link];
    })
  );
  for (const [roleId, link] of currentRoleIds.entries()) {
    if (!desiredIds.has(roleId) && link) {
      em.remove(link);
    }
  }
  for (const role of resolvedRoles) {
    if (!currentRoleIds.has(String(role.id))) {
      em.persist(em.create(UserRole, { user, role, createdAt: /* @__PURE__ */ new Date() }));
    }
  }
  await em.flush();
}
async function loadUserRoleNames(em, userId) {
  const links = await findWithDecryption(
    em,
    UserRole,
    { user: userId },
    { populate: ["role"] },
    { tenantId: null, organizationId: null }
  );
  const names = links.map((link) => link.role?.name ?? "").filter((name) => !!name);
  return Array.from(new Set(names)).sort((a, b) => a.localeCompare(b));
}
function serializeUser(user, roles, custom) {
  const payload = {
    email: String(user.email ?? ""),
    organizationId: user.organizationId ? String(user.organizationId) : null,
    tenantId: user.tenantId ? String(user.tenantId) : null,
    roles,
    name: user.name ? String(user.name) : null,
    isConfirmed: Boolean(user.isConfirmed)
  };
  if (custom && Object.keys(custom).length) payload.custom = custom;
  return payload;
}
function captureUserSnapshots(user, roles, acls = [], custom) {
  return {
    view: serializeUser(user, roles, custom),
    undo: {
      id: String(user.id),
      email: String(user.email ?? ""),
      organizationId: user.organizationId ? String(user.organizationId) : null,
      tenantId: user.tenantId ? String(user.tenantId) : null,
      passwordHash: user.passwordHash ? String(user.passwordHash) : null,
      name: user.name ? String(user.name) : null,
      isConfirmed: Boolean(user.isConfirmed),
      roles: [...roles],
      acls,
      ...custom && Object.keys(custom).length ? { custom } : {}
    }
  };
}
async function loadUserAclSnapshots(em, userId) {
  const list = await findWithDecryption(em, UserAcl, { user: userId }, {}, { tenantId: null, organizationId: null });
  return list.map((acl) => ({
    tenantId: String(acl.tenantId),
    features: Array.isArray(acl.featuresJson) ? [...acl.featuresJson] : null,
    isSuperAdmin: Boolean(acl.isSuperAdmin),
    organizations: Array.isArray(acl.organizationsJson) ? [...acl.organizationsJson] : null
  }));
}
async function restoreUserAcls(em, user, acls) {
  await em.nativeDelete(UserAcl, { user: String(user.id) });
  for (const acl of acls) {
    const entity = em.create(UserAcl, {
      user,
      tenantId: acl.tenantId,
      featuresJson: acl.features ?? null,
      isSuperAdmin: acl.isSuperAdmin,
      organizationsJson: acl.organizations ?? null,
      createdAt: /* @__PURE__ */ new Date()
    });
    em.persist(entity);
  }
  await em.flush();
}
async function loadUserCustomSnapshot(em, id, tenantId, organizationId) {
  return await loadCustomFieldSnapshot(em, {
    entityId: E.auth.user,
    recordId: id,
    tenantId,
    organizationId
  });
}
async function invalidateUserCache(ctx, userId) {
  try {
    const rbacService = ctx.container.resolve("rbacService");
    await rbacService.invalidateUserCache(userId);
  } catch {
  }
  try {
    const cache = ctx.container.resolve("cache");
    if (cache?.deleteByTags) await cache.deleteByTags([`rbac:user:${userId}`]);
  } catch {
  }
}
function diffRoleChanges(before, after) {
  const beforeSet = new Set(before);
  const afterSet = new Set(after);
  const assigned = after.filter((role) => !beforeSet.has(role));
  const revoked = before.filter((role) => !afterSet.has(role));
  return { assigned, revoked };
}
function arrayEquals(left, right) {
  if (!left) return false;
  if (left.length !== right.length) return false;
  return left.every((value, idx) => value === right[idx]);
}
async function resolveUserTenantId(em, id) {
  const existing = await findOneWithDecryption(em, User, { id, deletedAt: null }, {}, { tenantId: null, organizationId: null });
  return existing?.tenantId ? String(existing.tenantId) : null;
}
async function throwDuplicateEmailError() {
  const { translate } = await resolveTranslations();
  const message = translate("auth.users.errors.emailExists", "Email already in use");
  throw new CrudHttpError(400, {
    error: message,
    fieldErrors: { email: message },
    details: [{ path: ["email"], message, code: "duplicate", origin: "validation" }]
  });
}
export {
  userCrudEvents,
  userCrudIndexer
};
//# sourceMappingURL=users.js.map
