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 { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { z } from "zod";
import { Role, RoleAcl, UserRole } from "@open-mercato/core/modules/auth/data/entities";
import { E } from "../../../generated/entities.ids.generated.js";
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 { resolveIsSuperAdmin, normalizeTenantId } from "@open-mercato/core/modules/auth/lib/tenantAccess";
function resolveActorTenantScope(ctx) {
  if (ctx.systemActor === true) return null;
  const auth = ctx.auth;
  if (!auth) return null;
  if (auth.isSuperAdmin === true) return null;
  return normalizeTenantId(auth.tenantId ?? null) ?? null;
}
function assertRoleTenantInScope(actorTenantScope, targetTenantId) {
  if (!actorTenantScope) return;
  const targetTenant = normalizeTenantId(targetTenantId) ?? null;
  if (!targetTenant || targetTenant !== actorTenantScope) {
    throw new CrudHttpError(404, { error: "Role not found" });
  }
}
const RESERVED_ROLE_NAMES = /* @__PURE__ */ new Set(["superadmin", "admin"]);
function isReservedRoleName(name) {
  if (typeof name !== "string") return false;
  const normalized = name.trim().toLowerCase();
  return normalized.length > 0 && RESERVED_ROLE_NAMES.has(normalized);
}
function assertRoleNameAllowed(name) {
  if (isReservedRoleName(name)) {
    throw new CrudHttpError(400, { error: "Role name is reserved" });
  }
}
async function resolveActorScope(ctx) {
  const isSuperAdmin = await resolveIsSuperAdmin(ctx);
  const actorTenantId = normalizeTenantId(ctx.auth?.tenantId ?? null) ?? null;
  return { isSuperAdmin, actorTenantId };
}
function buildScopedRoleFilter(roleId, scope) {
  const filter = { id: roleId, deletedAt: null };
  if (!scope.isSuperAdmin) {
    ;
    filter.tenantId = scope.actorTenantId;
  }
  return filter;
}
const createSchema = z.object({
  name: z.string().min(2).max(100),
  tenantId: z.string().uuid().optional()
});
const updateSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(100).optional(),
  tenantId: z.string().uuid().optional()
});
const roleCrudEvents = {
  module: "auth",
  entity: "role",
  persistent: true,
  buildPayload: (ctx) => ({
    id: ctx.identifiers.id,
    tenantId: ctx.identifiers.tenantId
  })
};
const roleCrudIndexer = {
  entityType: E.auth.role,
  buildUpsertPayload: (ctx) => ({
    entityType: E.auth.role,
    recordId: ctx.identifiers.id,
    tenantId: ctx.identifiers.tenantId
  }),
  buildDeletePayload: (ctx) => ({
    entityType: E.auth.role,
    recordId: ctx.identifiers.id,
    tenantId: ctx.identifiers.tenantId
  })
};
const createRoleCommand = {
  id: "auth.roles.create",
  async execute(rawInput, ctx) {
    const rawBody = rawInput && typeof rawInput === "object" ? rawInput : {};
    if ("tenantId" in rawBody && rawBody.tenantId === null) {
      throw new CrudHttpError(400, { error: "tenantId cannot be null \u2014 global roles are not supported" });
    }
    const { parsed, custom } = parseWithCustomFields(createSchema, rawInput);
    assertRoleNameAllowed(parsed.name);
    const scope = await resolveActorScope(ctx);
    const requestedTenantId = parsed.tenantId ?? null;
    if (!scope.isSuperAdmin && requestedTenantId && requestedTenantId !== scope.actorTenantId) {
      throw new CrudHttpError(403, { error: "Not authorized to target this tenant." });
    }
    const resolvedTenantId = scope.isSuperAdmin ? requestedTenantId ?? scope.actorTenantId : scope.actorTenantId;
    if (!resolvedTenantId) {
      throw new CrudHttpError(400, { error: "tenantId is required \u2014 global roles are not supported" });
    }
    const de = ctx.container.resolve("dataEngine");
    const role = await de.createOrmEntity({
      entity: Role,
      data: {
        name: parsed.name,
        tenantId: resolvedTenantId
      }
    });
    await setCustomFieldsIfAny({
      dataEngine: de,
      entityId: E.auth.role,
      recordId: String(role.id),
      organizationId: null,
      tenantId: resolvedTenantId,
      values: custom
    });
    await emitCrudSideEffects({
      dataEngine: de,
      action: "created",
      entity: role,
      identifiers: {
        id: String(role.id),
        organizationId: null,
        tenantId: resolvedTenantId
      },
      events: roleCrudEvents,
      indexer: roleCrudIndexer
    });
    return role;
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    const custom = await loadCustomFieldSnapshot(em, {
      entityId: E.auth.role,
      recordId: String(result.id),
      tenantId: result.tenantId ? String(result.tenantId) : null
    });
    return serializeRole(result, custom);
  },
  buildLog: async ({ result, ctx }) => {
    const { translate } = await resolveTranslations();
    const em = ctx.container.resolve("em").fork();
    const custom = await loadCustomFieldSnapshot(em, {
      entityId: E.auth.role,
      recordId: String(result.id),
      tenantId: result.tenantId ? String(result.tenantId) : null
    });
    const snapshot = captureRoleSnapshots(result, [], custom);
    return {
      actionLabel: translate("auth.audit.roles.create", "Create role"),
      resourceKind: "auth.role",
      resourceId: String(result.id),
      tenantId: result.tenantId ? String(result.tenantId) : null,
      snapshotAfter: snapshot.view,
      payload: {
        undo: {
          after: snapshot.undo
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry)?.after;
    if (!undo) return;
    const em = ctx.container.resolve("em");
    const de = ctx.container.resolve("dataEngine");
    await em.nativeDelete(RoleAcl, { role: undo.id });
    if (undo.custom && Object.keys(undo.custom).length) {
      const reset = buildCustomFieldResetMap(void 0, undo.custom);
      if (Object.keys(reset).length) {
        await setCustomFieldsIfAny({
          dataEngine: de,
          entityId: E.auth.role,
          recordId: undo.id,
          organizationId: null,
          tenantId: undo.tenantId ?? null,
          values: reset,
          notify: false
        });
      }
    }
    await de.deleteOrmEntity({
      entity: Role,
      where: { id: undo.id, deletedAt: null },
      soft: false
    });
  },
  redo: async ({ logEntry, ctx }) => {
    const after = resolveRedoSnapshot(logEntry);
    if (!after) throw new CrudHttpError(400, { error: "[internal] redo snapshot unavailable for role create" });
    const em = ctx.container.resolve("em");
    const de = ctx.container.resolve("dataEngine");
    let role = await findOneWithDecryption(em, Role, { id: after.id }, {}, { tenantId: null, organizationId: null });
    if (role) {
      role.deletedAt = null;
      role.name = after.name;
      role.tenantId = after.tenantId;
      await em.flush();
    } else {
      role = await de.createOrmEntity({
        entity: Role,
        data: {
          id: after.id,
          name: after.name,
          tenantId: after.tenantId
        }
      });
    }
    await restoreRoleAcls(em, after.id, 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.role,
          recordId: after.id,
          organizationId: null,
          tenantId: after.tenantId ?? null,
          values: reset,
          notify: false
        });
      }
    }
    await emitCrudSideEffects({
      dataEngine: de,
      action: "created",
      entity: role,
      identifiers: {
        id: after.id,
        organizationId: null,
        tenantId: after.tenantId ?? null
      },
      events: roleCrudEvents,
      indexer: roleCrudIndexer
    });
    return role;
  }
};
const updateRoleCommand = {
  id: "auth.roles.update",
  async prepare(rawInput, ctx) {
    const { parsed } = parseWithCustomFields(updateSchema, rawInput);
    const em = ctx.container.resolve("em");
    const scope = await resolveActorScope(ctx);
    const existing = await findOneWithDecryption(em, Role, buildScopedRoleFilter(parsed.id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null });
    if (!existing) throw new CrudHttpError(404, { error: "Role not found" });
    assertRoleTenantInScope(resolveActorTenantScope(ctx), existing.tenantId);
    const acls = await loadRoleAclSnapshots(em, parsed.id);
    const custom = await loadCustomFieldSnapshot(em, {
      entityId: E.auth.role,
      recordId: parsed.id,
      tenantId: existing.tenantId ? String(existing.tenantId) : null
    });
    return { before: captureRoleSnapshots(existing, acls, custom) };
  },
  async execute(rawInput, ctx) {
    const { parsed, custom } = parseWithCustomFields(updateSchema, rawInput);
    const em = ctx.container.resolve("em");
    const scope = await resolveActorScope(ctx);
    const current = await findOneWithDecryption(em, Role, buildScopedRoleFilter(parsed.id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null });
    if (!current) throw new CrudHttpError(404, { error: "Role not found" });
    const actorTenantScope = resolveActorTenantScope(ctx);
    assertRoleTenantInScope(actorTenantScope, current.tenantId);
    if (parsed.name !== void 0) {
      const nextName = parsed.name;
      if (nextName !== current.name) assertRoleNameAllowed(nextName);
      if (nextName !== current.name) {
        const assignments = await em.count(UserRole, { role: current, deletedAt: null });
        if (assignments > 0) {
          throw new CrudHttpError(400, { error: "Role name cannot be changed while users are assigned" });
        }
      }
    }
    const wantsTenantChange = parsed.tenantId !== void 0 && parsed.tenantId !== current.tenantId;
    if (wantsTenantChange) {
      if (!scope.isSuperAdmin) {
        throw new CrudHttpError(403, { error: "Not authorized to target this tenant." });
      }
      const assignments = await em.count(UserRole, { role: current, deletedAt: null });
      if (assignments > 0) {
        throw new CrudHttpError(400, { error: "Role cannot be moved to another tenant while users are assigned" });
      }
      await em.nativeDelete(RoleAcl, { role: parsed.id });
    }
    const de = ctx.container.resolve("dataEngine");
    const role = await de.updateOrmEntity({
      entity: Role,
      where: buildScopedRoleFilter(parsed.id, scope),
      apply: (entity) => {
        if (parsed.name !== void 0) entity.name = parsed.name;
        if (parsed.tenantId !== void 0 && scope.isSuperAdmin) entity.tenantId = parsed.tenantId;
      }
    });
    if (!role) throw new CrudHttpError(404, { error: "Role not found" });
    await setCustomFieldsIfAny({
      dataEngine: de,
      entityId: E.auth.role,
      recordId: String(role.id),
      organizationId: null,
      tenantId: role.tenantId ? String(role.tenantId) : null,
      values: custom
    });
    await emitCrudSideEffects({
      dataEngine: de,
      action: "updated",
      entity: role,
      identifiers: {
        id: String(role.id),
        organizationId: null,
        tenantId: role.tenantId ? String(role.tenantId) : null
      },
      events: roleCrudEvents,
      indexer: roleCrudIndexer
    });
    return role;
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    const custom = await loadCustomFieldSnapshot(em, {
      entityId: E.auth.role,
      recordId: String(result.id),
      tenantId: result.tenantId ? String(result.tenantId) : null
    });
    return serializeRole(result, 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 afterAcls = await loadRoleAclSnapshots(em, String(result.id));
    const custom = await loadCustomFieldSnapshot(em, {
      entityId: E.auth.role,
      recordId: String(result.id),
      tenantId: result.tenantId ? String(result.tenantId) : null
    });
    const afterSnapshots = captureRoleSnapshots(result, afterAcls, custom);
    const after = afterSnapshots.view;
    const changes = buildChanges(before ?? null, after, ["name", "tenantId"]);
    const customDiff = diffCustomFieldChanges(before?.custom, custom);
    for (const [key, diff] of Object.entries(customDiff)) {
      changes[`cf_${key}`] = diff;
    }
    return {
      actionLabel: translate("auth.audit.roles.update", "Update role"),
      resourceKind: "auth.role",
      resourceId: String(result.id),
      tenantId: result.tenantId ? String(result.tenantId) : null,
      changes,
      snapshotBefore: before ?? null,
      snapshotAfter: after,
      payload: {
        undo: {
          before: beforeUndo,
          after: afterSnapshots.undo
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const before = undo?.before;
    const after = undo?.after;
    if (!before) return;
    const em = ctx.container.resolve("em");
    const de = ctx.container.resolve("dataEngine");
    const updated = await de.updateOrmEntity({
      entity: Role,
      where: { id: before.id, deletedAt: null },
      apply: (entity) => {
        entity.name = before.name;
        entity.tenantId = before.tenantId;
      }
    });
    if (updated) {
      await restoreRoleAcls(em, before.id, before.acls);
    }
    const reset = buildCustomFieldResetMap(before.custom, after?.custom);
    if (Object.keys(reset).length) {
      await setCustomFieldsIfAny({
        dataEngine: de,
        entityId: E.auth.role,
        recordId: before.id,
        organizationId: null,
        tenantId: before.tenantId,
        values: reset,
        notify: false
      });
    }
    await emitCrudUndoSideEffects({
      dataEngine: de,
      action: "updated",
      entity: updated,
      identifiers: {
        id: before.id,
        organizationId: null,
        tenantId: before.tenantId ?? null
      },
      events: roleCrudEvents,
      indexer: roleCrudIndexer
    });
  }
};
const deleteRoleCommand = {
  id: "auth.roles.delete",
  async prepare(input, ctx) {
    const id = requireId(input, "Role id required");
    const em = ctx.container.resolve("em");
    const scope = await resolveActorScope(ctx);
    const existing = await findOneWithDecryption(em, Role, buildScopedRoleFilter(id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null });
    if (!existing) return {};
    const actorTenantScope = resolveActorTenantScope(ctx);
    if (actorTenantScope) {
      const targetTenant = normalizeTenantId(existing.tenantId) ?? null;
      if (!targetTenant || targetTenant !== actorTenantScope) return {};
    }
    const acls = await loadRoleAclSnapshots(em, id);
    const custom = await loadCustomFieldSnapshot(em, {
      entityId: E.auth.role,
      recordId: id,
      tenantId: existing.tenantId ? String(existing.tenantId) : null
    });
    return { before: captureRoleSnapshots(existing, acls, custom) };
  },
  async execute(input, ctx) {
    const id = requireId(input, "Role id required");
    const em = ctx.container.resolve("em");
    const scope = await resolveActorScope(ctx);
    const role = await findOneWithDecryption(em, Role, buildScopedRoleFilter(id, scope), {}, { tenantId: scope.actorTenantId, organizationId: null });
    if (!role) throw new CrudHttpError(404, { error: "Role not found" });
    assertRoleTenantInScope(resolveActorTenantScope(ctx), role.tenantId);
    const activeAssignments = await em.count(UserRole, { role, deletedAt: null });
    if (activeAssignments > 0) throw new CrudHttpError(400, { error: "Role has assigned users" });
    await em.nativeDelete(RoleAcl, { role: id });
    const de = ctx.container.resolve("dataEngine");
    const deleted = await de.deleteOrmEntity({
      entity: Role,
      where: buildScopedRoleFilter(id, scope),
      soft: false
    });
    if (!deleted) throw new CrudHttpError(404, { error: "Role not found" });
    await emitCrudSideEffects({
      dataEngine: de,
      action: "deleted",
      entity: deleted,
      identifiers: {
        id,
        organizationId: null,
        tenantId: deleted.tenantId ? String(deleted.tenantId) : null
      },
      events: roleCrudEvents,
      indexer: roleCrudIndexer
    });
    return deleted;
  },
  buildLog: async ({ snapshots, input }) => {
    const { translate } = await resolveTranslations();
    const beforeSnapshots = snapshots.before;
    const before = beforeSnapshots?.view;
    const beforeUndo = beforeSnapshots?.undo ?? null;
    const id = requireId(input, "Role id required");
    return {
      actionLabel: translate("auth.audit.roles.delete", "Delete role"),
      resourceKind: "auth.role",
      resourceId: id,
      tenantId: before?.tenantId ?? null,
      snapshotBefore: before ?? null,
      payload: {
        undo: {
          before: beforeUndo
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const before = extractUndoPayload(logEntry)?.before;
    if (!before) return;
    const em = ctx.container.resolve("em");
    const de = ctx.container.resolve("dataEngine");
    let role = await findOneWithDecryption(em, Role, { id: before.id }, {}, { tenantId: null, organizationId: null });
    if (role) {
      role.deletedAt = null;
      role.name = before.name;
      role.tenantId = before.tenantId;
      await em.flush();
    } else {
      role = await de.createOrmEntity({
        entity: Role,
        data: {
          id: before.id,
          name: before.name,
          tenantId: before.tenantId
        }
      });
    }
    await restoreRoleAcls(em, before.id, before.acls);
    const reset = buildCustomFieldResetMap(before.custom, void 0);
    if (Object.keys(reset).length) {
      await setCustomFieldsIfAny({
        dataEngine: de,
        entityId: E.auth.role,
        recordId: before.id,
        organizationId: null,
        tenantId: before.tenantId ?? null,
        values: reset,
        notify: false
      });
    }
    await emitCrudUndoSideEffects({
      dataEngine: de,
      action: "updated",
      entity: role,
      identifiers: {
        id: before.id,
        organizationId: null,
        tenantId: before.tenantId ?? null
      },
      events: roleCrudEvents,
      indexer: roleCrudIndexer
    });
  }
};
registerCommand(createRoleCommand);
registerCommand(updateRoleCommand);
registerCommand(deleteRoleCommand);
function serializeRole(role, custom) {
  const payload = {
    name: String(role.name ?? ""),
    tenantId: String(role.tenantId)
  };
  if (custom && Object.keys(custom).length) payload.custom = custom;
  return payload;
}
function captureRoleSnapshots(role, acls = [], custom) {
  return {
    view: serializeRole(role, custom),
    undo: {
      id: String(role.id),
      name: String(role.name ?? ""),
      tenantId: String(role.tenantId),
      acls,
      ...custom && Object.keys(custom).length ? { custom } : {}
    }
  };
}
async function loadRoleAclSnapshots(em, roleId) {
  const entries = await findWithDecryption(em, RoleAcl, { role: roleId }, {}, { tenantId: null, organizationId: null });
  return entries.map((entry) => ({
    id: entry.id ? String(entry.id) : null,
    tenantId: String(entry.tenantId),
    features: Array.isArray(entry.featuresJson) ? [...entry.featuresJson] : null,
    isSuperAdmin: Boolean(entry.isSuperAdmin),
    organizations: Array.isArray(entry.organizationsJson) ? [...entry.organizationsJson] : null
  }));
}
async function restoreRoleAcls(em, roleId, acls) {
  await em.nativeDelete(RoleAcl, { role: roleId });
  if (!acls.length) {
    await em.flush();
    return;
  }
  const roleRef = em.getReference(Role, roleId);
  for (const acl of acls) {
    const entity = em.create(RoleAcl, {
      id: acl.id ?? void 0,
      role: roleRef,
      tenantId: acl.tenantId,
      featuresJson: acl.features ?? null,
      isSuperAdmin: acl.isSuperAdmin,
      organizationsJson: acl.organizations ?? null,
      createdAt: /* @__PURE__ */ new Date()
    });
    em.persist(entity);
  }
  await em.flush();
}
export {
  roleCrudEvents,
  roleCrudIndexer
};
//# sourceMappingURL=roles.js.map
