import { hash } from "bcryptjs";
import { randomBytes } from "node:crypto";
import { Role, RoleAcl, User, UserRole } from "@open-mercato/core/modules/auth/data/entities";
import { Tenant, Organization } from "@open-mercato/core/modules/directory/data/entities";
import { rebuildHierarchyForTenant } from "@open-mercato/core/modules/directory/lib/hierarchy";
import { normalizeTenantId } from "./tenantAccess.js";
import { computeEmailHash } from "@open-mercato/core/modules/auth/lib/emailHash";
import { getDefaultEncryptionMaps } from "@open-mercato/shared/modules/registry";
import { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from "@open-mercato/shared/lib/encryption/toggles";
import { EncryptionMap } from "@open-mercato/core/modules/entities/data/entities";
import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
import { TenantDataEncryptionService } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("auth").child({ component: "setup" });
const DEFAULT_ROLE_NAMES = ["employee", "admin", "superadmin"];
const DEMO_SUPERADMIN_EMAIL = "superadmin@acme.com";
const DEFAULT_DERIVED_EMAIL_DOMAIN = DEMO_SUPERADMIN_EMAIL.split("@")[1] ?? "acme.com";
async function ensureRolesInContext(em, roleNames, tenantId) {
  for (const name of roleNames) {
    const existing = await findOneWithDecryption(em, Role, { name, tenantId }, {}, { tenantId, organizationId: null });
    if (existing) continue;
    em.persist(em.create(Role, { name, tenantId, createdAt: /* @__PURE__ */ new Date() }));
  }
}
async function ensureRoles(em, options = {}) {
  const roleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES];
  const tenantId = normalizeTenantId(options.tenantId ?? null);
  if (!tenantId) {
    throw new Error("ensureRoles requires a tenantId \u2014 global roles are not supported");
  }
  await em.transactional(async (tem) => {
    await ensureRolesInContext(tem, roleNames, tenantId);
    await tem.flush();
  });
}
async function findRoleByName(em, name, tenantId) {
  const normalizedTenant = normalizeTenantId(tenantId ?? null) ?? null;
  return findOneWithDecryption(em, Role, { name, tenantId: normalizedTenant }, {}, { tenantId: normalizedTenant, organizationId: null });
}
async function findRoleByNameOrFail(em, name, tenantId) {
  const role = await findRoleByName(em, name, tenantId);
  if (!role) throw new Error(`ROLE_NOT_FOUND:${name}`);
  return role;
}
const DERIVED_EMAIL_ENV = {
  admin: "OM_INIT_ADMIN_EMAIL",
  employee: "OM_INIT_EMPLOYEE_EMAIL"
};
function resolveDemoUserEmails() {
  const adminEmail = readEnvValue(DERIVED_EMAIL_ENV.admin) ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`;
  const employeeEmail = readEnvValue(DERIVED_EMAIL_ENV.employee) ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`;
  return [
    { role: "superadmin", email: DEMO_SUPERADMIN_EMAIL },
    { role: "admin", email: adminEmail },
    { role: "employee", email: employeeEmail }
  ];
}
class OrgSlugExistsError extends Error {
  constructor(slug) {
    super(`ORG_SLUG_EXISTS: an organization with slug "${slug}" already exists`);
    this.slug = slug;
    this.name = "OrgSlugExistsError";
  }
}
class DerivedUserPasswordRequiredError extends Error {
  constructor(missing) {
    super(
      `DERIVED_USER_PASSWORD_REQUIRED: missing ${missing.join(", ")} (set the env vars, pass allowDemoDerivedPasswords: true / --include-demo-users in non-production, or disable derived user seeding)`
    );
    this.missing = missing;
    this.name = "DerivedUserPasswordRequiredError";
  }
}
async function setupInitialTenant(em, options) {
  const {
    primaryUser,
    includeDerivedUsers = true,
    failIfUserExists = false,
    primaryUserRoles,
    includeSuperadminRole = true
  } = options;
  const primaryRolesInput = primaryUserRoles && primaryUserRoles.length ? primaryUserRoles : ["superadmin"];
  const primaryRoles = includeSuperadminRole ? primaryRolesInput : primaryRolesInput.filter((role) => role !== "superadmin");
  if (primaryRoles.length === 0) {
    throw new Error("PRIMARY_ROLES_REQUIRED");
  }
  const defaultRoleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES];
  const resolvedRoleNames = includeSuperadminRole ? defaultRoleNames : defaultRoleNames.filter((role) => role !== "superadmin");
  const roleNames = Array.from(/* @__PURE__ */ new Set([...resolvedRoleNames, ...primaryRoles]));
  const resolvedModules = options.modules ?? tryGetModules();
  const defaultEncryptionMaps = getDefaultEncryptionMaps(resolvedModules);
  const mainEmail = primaryUser.email;
  const existingUser = await findOneWithDecryption(em, User, { email: mainEmail }, {}, { tenantId: null, organizationId: null });
  if (existingUser && failIfUserExists) {
    throw new Error("USER_EXISTS");
  }
  if (options.orgSlug) {
    const slugConflict = await findOneWithDecryption(
      em,
      Organization,
      { slug: options.orgSlug },
      {},
      { tenantId: null, organizationId: null }
    );
    if (slugConflict) {
      throw new OrgSlugExistsError(options.orgSlug);
    }
  }
  let tenantId;
  let organizationId;
  let reusedExistingUser = false;
  const userSnapshots = [];
  await em.transactional(async (tem) => {
    if (!existingUser) return;
    reusedExistingUser = true;
    tenantId = existingUser.tenantId ? String(existingUser.tenantId) : void 0;
    organizationId = existingUser.organizationId ? String(existingUser.organizationId) : void 0;
    const roleTenantId = normalizeTenantId(existingUser.tenantId ?? null) ?? null;
    if (!roleTenantId) {
      throw new Error("Cannot reuse a user without a tenantId \u2014 global roles are not supported");
    }
    await ensureRolesInContext(tem, roleNames, roleTenantId);
    await tem.flush();
    const requiredRoleSet = /* @__PURE__ */ new Set([...roleNames, ...primaryRoles]);
    const links = await findWithDecryption(
      tem,
      UserRole,
      { user: existingUser },
      { populate: ["role"] },
      { tenantId: roleTenantId, organizationId: null }
    );
    const currentRoles = new Set(links.map((link) => link.role.name));
    for (const roleName of requiredRoleSet) {
      if (!currentRoles.has(roleName)) {
        const role = await findRoleByNameOrFail(tem, roleName, roleTenantId);
        tem.persist(tem.create(UserRole, { user: existingUser, role, createdAt: /* @__PURE__ */ new Date() }));
      }
    }
    await tem.flush();
    const roles = Array.from(/* @__PURE__ */ new Set([...currentRoles, ...roleNames]));
    userSnapshots.push({ user: existingUser, roles, created: false });
  });
  if (!existingUser) {
    const baseUsers = [
      { email: primaryUser.email, roles: primaryRoles, name: resolvePrimaryName(primaryUser) }
    ];
    if (includeDerivedUsers) {
      const adminOverride = readEnvValue(DERIVED_EMAIL_ENV.admin);
      const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee);
      const adminEmail = adminOverride ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`;
      const employeeEmail = employeeOverride ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`;
      const envAdminPwd = readEnvValue("OM_INIT_ADMIN_PASSWORD");
      const envEmployeePwd = readEnvValue("OM_INIT_EMPLOYEE_PASSWORD");
      const isProduction = process.env.NODE_ENV === "production";
      const allowDemo = options.allowDemoDerivedPasswords === true;
      if (isProduction && !allowDemo) {
        const missing = [];
        if (!envAdminPwd) missing.push("OM_INIT_ADMIN_PASSWORD");
        if (!envEmployeePwd) missing.push("OM_INIT_EMPLOYEE_PASSWORD");
        if (missing.length) {
          throw new DerivedUserPasswordRequiredError(missing);
        }
      }
      const fallbackAdminPwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD;
      const fallbackEmployeePwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD;
      const adminPasswordPlain = envAdminPwd ?? fallbackAdminPwd;
      const employeePasswordPlain = envEmployeePwd ?? fallbackEmployeePwd;
      const adminPasswordHash = await hash(adminPasswordPlain, 10);
      const employeePasswordHash = await hash(employeePasswordPlain, 10);
      addUniqueBaseUser(baseUsers, {
        email: adminEmail,
        roles: ["admin"],
        passwordHash: adminPasswordHash,
        generatedPassword: envAdminPwd ? null : adminPasswordPlain
      });
      addUniqueBaseUser(baseUsers, {
        email: employeeEmail,
        roles: ["employee"],
        passwordHash: employeePasswordHash,
        generatedPassword: envEmployeePwd ? null : employeePasswordPlain
      });
    }
    const passwordHash = await resolvePasswordHash(primaryUser);
    await em.transactional(async (tem) => {
      const tenant = tem.create(Tenant, {
        name: options.orgName,
        isActive: true,
        createdAt: /* @__PURE__ */ new Date(),
        updatedAt: /* @__PURE__ */ new Date()
      });
      tem.persist(tenant);
      await tem.flush();
      const organization = tem.create(Organization, {
        name: options.orgName,
        slug: options.orgSlug ?? null,
        tenant,
        isActive: true,
        depth: 0,
        ancestorIds: [],
        childIds: [],
        descendantIds: [],
        createdAt: /* @__PURE__ */ new Date(),
        updatedAt: /* @__PURE__ */ new Date()
      });
      tem.persist(organization);
      await tem.flush();
      tenantId = String(tenant.id);
      organizationId = String(organization.id);
      const roleTenantId = tenantId;
      if (isTenantDataEncryptionEnabled()) {
        try {
          const kms = createKmsService();
          if (kms.isHealthy()) {
            if (isEncryptionDebugEnabled()) {
              logger.info("Provisioning tenant DEK", { tenantId: String(tenant.id) });
            }
            await kms.createTenantDek(String(tenant.id));
            if (isEncryptionDebugEnabled()) {
              logger.info("Created tenant DEK during setup", { tenantId: String(tenant.id) });
            }
          } else {
            if (isEncryptionDebugEnabled()) {
              logger.warn("KMS not healthy, skipping tenant DEK creation", { tenantId: String(tenant.id) });
            }
          }
        } catch (err) {
          if (isEncryptionDebugEnabled()) {
            logger.warn("Failed to create tenant DEK", { err });
          }
        }
      }
      await ensureRolesInContext(tem, roleNames, roleTenantId);
      await tem.flush();
      if (isTenantDataEncryptionEnabled()) {
        for (const spec of defaultEncryptionMaps) {
          const existing = await findOneWithDecryption(tem, EncryptionMap, { entityId: spec.entityId, tenantId: tenant.id, organizationId: organization.id, deletedAt: null }, {}, { tenantId: String(tenant.id), organizationId: String(organization.id) });
          if (!existing) {
            tem.persist(tem.create(EncryptionMap, {
              entityId: spec.entityId,
              tenantId: tenant.id,
              organizationId: organization.id,
              fieldsJson: spec.fields,
              isActive: true,
              createdAt: /* @__PURE__ */ new Date(),
              updatedAt: /* @__PURE__ */ new Date()
            }));
          } else {
            existing.fieldsJson = spec.fields;
            existing.isActive = true;
          }
        }
        await tem.flush();
      }
    });
    await em.transactional(async (tem) => {
      if (!tenantId || !organizationId) return;
      const roleTenantId = tenantId;
      const encryptionService = isTenantDataEncryptionEnabled() ? new TenantDataEncryptionService(tem, { kms: createKmsService() }) : null;
      if (encryptionService) {
        await encryptionService.invalidateMap("auth:user", String(tenantId), String(organizationId));
        await encryptionService.invalidateMap("auth:user", String(tenantId), null);
      }
      for (const base of baseUsers) {
        const resolvedPasswordHash = base.passwordHash ?? passwordHash;
        let user = await findOneWithDecryption(tem, User, { email: base.email }, {}, { tenantId: tenantId ?? null, organizationId: organizationId ?? null });
        const confirm = primaryUser.confirm ?? true;
        const encryptedPayload = encryptionService ? await encryptionService.encryptEntityPayload("auth:user", { email: base.email }, tenantId, organizationId) : { email: base.email, emailHash: computeEmailHash(base.email) };
        if (user) {
          user.passwordHash = resolvedPasswordHash;
          user.organizationId = organizationId;
          user.tenantId = tenantId;
          if (isTenantDataEncryptionEnabled()) {
            user.email = encryptedPayload.email;
            user.emailHash = encryptedPayload.emailHash ?? computeEmailHash(base.email);
          }
          if (base.name) user.name = base.name;
          if (confirm) user.isConfirmed = true;
          tem.persist(user);
          userSnapshots.push({ user, roles: base.roles, created: false, generatedPassword: base.generatedPassword ?? null });
        } else {
          user = tem.create(User, {
            email: encryptedPayload.email ?? base.email,
            emailHash: isTenantDataEncryptionEnabled() ? encryptedPayload.emailHash ?? computeEmailHash(base.email) : void 0,
            passwordHash: resolvedPasswordHash,
            organizationId,
            tenantId,
            name: base.name ?? void 0,
            isConfirmed: confirm,
            createdAt: /* @__PURE__ */ new Date()
          });
          tem.persist(user);
          userSnapshots.push({ user, roles: base.roles, created: true, generatedPassword: base.generatedPassword ?? null });
        }
        await tem.flush();
        for (const roleName of base.roles) {
          const role = await findRoleByNameOrFail(tem, roleName, roleTenantId);
          const existingLink = await findOneWithDecryption(tem, UserRole, { user, role }, {}, { tenantId: tenantId ?? null, organizationId: null });
          if (!existingLink) tem.persist(tem.create(UserRole, { user, role, createdAt: /* @__PURE__ */ new Date() }));
        }
        await tem.flush();
      }
    });
  }
  if (!tenantId || !organizationId) {
    throw new Error("SETUP_FAILED");
  }
  if (!reusedExistingUser) {
    await rebuildHierarchyForTenant(em, tenantId);
  }
  await ensureDefaultRoleAcls(em, tenantId, resolvedModules, { includeSuperadminRole });
  await deactivateDemoUsersIfSelfOnboardingEnabled(em);
  for (const mod of resolvedModules) {
    if (mod.setup?.onTenantCreated) {
      await mod.setup.onTenantCreated({ em, tenantId, organizationId });
    }
  }
  return {
    tenantId,
    organizationId,
    users: userSnapshots,
    reusedExistingUser
  };
}
function resolvePrimaryName(input) {
  if (input.displayName && input.displayName.trim()) return input.displayName.trim();
  const parts = [input.firstName, input.lastName].map((value) => value?.trim()).filter(Boolean);
  if (parts.length) return parts.join(" ");
  return null;
}
function readEnvValue(key) {
  const value = process.env[key];
  if (typeof value !== "string") return void 0;
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : void 0;
}
function addUniqueBaseUser(baseUsers, entry) {
  if (!entry.email) return;
  const normalized = entry.email.toLowerCase();
  if (baseUsers.some((user) => user.email.toLowerCase() === normalized)) return;
  baseUsers.push(entry);
}
const DEMO_DERIVED_PASSWORD = "secret";
function generateDerivedPassword() {
  return randomBytes(12).toString("base64url");
}
function isDemoModeEnabled() {
  const parsed = parseBooleanToken(process.env.DEMO_MODE ?? "");
  return parsed === false ? false : true;
}
function shouldKeepDemoSuperadminDuringInit() {
  if (process.env.OM_INIT_FLOW !== "true") return false;
  if (!readEnvValue("OM_INIT_SUPERADMIN_EMAIL")) return false;
  return isDemoModeEnabled();
}
async function resolvePasswordHash(input) {
  if (typeof input.hashedPassword === "string") return input.hashedPassword;
  if (input.password) return hash(input.password, 10);
  return null;
}
async function ensureDefaultRoleAcls(em, tenantId, modules, options = {}) {
  const includeSuperadminRole = options.includeSuperadminRole ?? true;
  const roleTenantId = normalizeTenantId(tenantId) ?? null;
  const superadminRole = includeSuperadminRole ? await findRoleByName(em, "superadmin", roleTenantId) : null;
  const adminRole = await findRoleByName(em, "admin", roleTenantId);
  const employeeRole = await findRoleByName(em, "employee", roleTenantId);
  const builtInRoles = ["superadmin", "admin", "employee"];
  const superadminFeatures = [];
  const adminFeatures = [];
  const employeeFeatures = [];
  const customRoleFeatures = /* @__PURE__ */ new Map();
  for (const mod of modules) {
    const roleFeatures = mod.setup?.defaultRoleFeatures;
    if (!roleFeatures) continue;
    if (roleFeatures.superadmin) superadminFeatures.push(...roleFeatures.superadmin);
    if (roleFeatures.admin) adminFeatures.push(...roleFeatures.admin);
    if (roleFeatures.employee) employeeFeatures.push(...roleFeatures.employee);
    for (const [roleName, features] of Object.entries(roleFeatures)) {
      if (builtInRoles.includes(roleName)) continue;
      if (!Array.isArray(features)) continue;
      const existing = customRoleFeatures.get(roleName) ?? [];
      existing.push(...features);
      customRoleFeatures.set(roleName, existing);
    }
  }
  logger.info("Seeded default role features", {
    superadmin: superadminFeatures,
    admin: adminFeatures,
    employee: employeeFeatures,
    ...customRoleFeatures.size > 0 ? Object.fromEntries(customRoleFeatures) : {}
  });
  if (includeSuperadminRole && superadminRole) {
    await ensureRoleAclFor(em, superadminRole, tenantId, superadminFeatures, { isSuperAdmin: true });
  }
  if (adminRole) {
    await ensureRoleAclFor(em, adminRole, tenantId, adminFeatures);
  }
  if (employeeRole) {
    await ensureRoleAclFor(em, employeeRole, tenantId, employeeFeatures);
  }
  for (const [roleName, features] of customRoleFeatures) {
    const role = await findRoleByName(em, roleName, roleTenantId);
    if (role) {
      await ensureRoleAclFor(em, role, tenantId, features);
    }
  }
}
async function ensureCustomRoleAcls(em, tenantId, modules) {
  const resolvedModules = modules ?? tryGetModules();
  const roleTenantId = normalizeTenantId(tenantId) ?? null;
  const builtInRoles = ["superadmin", "admin", "employee"];
  const customRoleFeatures = /* @__PURE__ */ new Map();
  for (const mod of resolvedModules) {
    const roleFeatures = mod.setup?.defaultRoleFeatures;
    if (!roleFeatures) continue;
    for (const [roleName, features] of Object.entries(roleFeatures)) {
      if (builtInRoles.includes(roleName)) continue;
      if (!Array.isArray(features)) continue;
      const existing = customRoleFeatures.get(roleName) ?? [];
      existing.push(...features);
      customRoleFeatures.set(roleName, existing);
    }
  }
  if (customRoleFeatures.size === 0) return;
  let seeded = 0;
  for (const [roleName, features] of customRoleFeatures) {
    const role = await findRoleByName(em, roleName, roleTenantId);
    if (role) {
      await ensureRoleAclFor(em, role, tenantId, features);
      seeded++;
    }
  }
  if (seeded > 0) {
    logger.info("Seeded custom role ACLs", { seeded });
  }
}
async function ensureRoleAclFor(em, role, tenantId, features, options = {}) {
  const existing = await findOneWithDecryption(em, RoleAcl, { role, tenantId }, {}, { tenantId, organizationId: null });
  if (!existing) {
    const acl = em.create(RoleAcl, {
      role,
      tenantId,
      featuresJson: features,
      isSuperAdmin: !!options.isSuperAdmin,
      createdAt: /* @__PURE__ */ new Date()
    });
    await em.persist(acl).flush();
    return;
  }
  const currentFeatures = Array.isArray(existing.featuresJson) ? existing.featuresJson : [];
  const merged = Array.from(/* @__PURE__ */ new Set([...currentFeatures, ...features]));
  const changed = merged.length !== currentFeatures.length || merged.some((value, index) => value !== currentFeatures[index]);
  if (changed) existing.featuresJson = merged;
  if (options.isSuperAdmin && !existing.isSuperAdmin) {
    existing.isSuperAdmin = true;
  }
  if (changed || options.isSuperAdmin) {
    await em.persist(existing).flush();
  }
}
async function deactivateDemoUsersIfSelfOnboardingEnabled(em) {
  if (process.env.SELF_SERVICE_ONBOARDING_ENABLED !== "true") return;
  if (shouldKeepDemoSuperadminDuringInit()) return;
  for (const { role, email } of resolveDemoUserEmails()) {
    try {
      const user = await findOneWithDecryption(
        em,
        User,
        { email },
        {},
        { tenantId: null, organizationId: null }
      );
      if (!user) continue;
      let dirty = false;
      if (user.passwordHash) {
        user.passwordHash = null;
        dirty = true;
      }
      if (user.isConfirmed !== false) {
        user.isConfirmed = false;
        dirty = true;
      }
      if (dirty) {
        await em.persist(user).flush();
      }
    } catch (error) {
      logger.error("Failed to deactivate demo user", { role, email, err: error });
    }
  }
}
const deactivateDemoSuperAdminIfSelfOnboardingEnabled = deactivateDemoUsersIfSelfOnboardingEnabled;
function tryGetModules() {
  try {
    const { getModules } = require("@open-mercato/shared/lib/modules/registry");
    return getModules();
  } catch {
    return [];
  }
}
export {
  DerivedUserPasswordRequiredError,
  OrgSlugExistsError,
  deactivateDemoUsersIfSelfOnboardingEnabled,
  ensureCustomRoleAcls,
  ensureDefaultRoleAcls,
  ensureRoles,
  resolveDemoUserEmails,
  setupInitialTenant
};
//# sourceMappingURL=setup-app.js.map
