import { getCurrentCacheTenant, runWithCacheTenant } from "@open-mercato/cache";
import { UserAcl, RoleAcl, User, UserRole } from "@open-mercato/core/modules/auth/data/entities";
import { ApiKey } from "@open-mercato/core/modules/api_keys/data/entities";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { buildOrgScopeUserCacheTag, buildOrgScopeTenantCacheTag } from "@open-mercato/core/modules/directory/utils/organizationScope";
import { matchFeature as sharedMatchFeature, hasAllFeatures as sharedHasAllFeatures } from "@open-mercato/shared/lib/auth/featureMatch";
import { filterGrantsByEnabledModules, getOwningModuleId, getEnabledModuleIds } from "@open-mercato/shared/security/enabledModulesRegistry";
function isAclData(value) {
  if (typeof value !== "object" || value === null) return false;
  const record = value;
  if (typeof record.isSuperAdmin !== "boolean") return false;
  if (!Array.isArray(record.features) || record.features.some((feature) => typeof feature !== "string")) return false;
  if (record.organizations !== null && record.organizations !== void 0) {
    if (!Array.isArray(record.organizations)) return false;
    if (record.organizations.some((org) => typeof org !== "string")) return false;
  }
  return true;
}
class RbacService {
  constructor(em, cache) {
    this.em = em;
    this.cacheTtlMs = 5 * 60 * 1e3;
    // 5 minutes default
    this.cache = null;
    this.globalSuperAdminCache = /* @__PURE__ */ new Map();
    this.cache = cache || null;
  }
  /**
   * Set cache TTL in milliseconds
   * @param ttlMs - Time to live in milliseconds
   */
  setCacheTtl(ttlMs) {
    this.cacheTtlMs = ttlMs;
  }
  /**
   * Checks if a required feature is satisfied by a granted feature permission.
   * 
   * Wildcard patterns:
   * - `*` (global wildcard): Grants access to all features
   * - `prefix.*` (module wildcard): Grants access to all features starting with `prefix.`
   *   and also the exact prefix itself (e.g., `entities.*` matches both `entities` and `entities.records.view`)
   * - Exact match: Feature must match exactly (e.g., `users.view` only matches `users.view`)
   * 
   * @param required - The feature being requested (e.g., 'entities.records.view')
   * @param granted - The feature permission granted (e.g., 'entities.*' or '*')
   * @returns true if the granted permission satisfies the required feature
   * 
   * @example
   * matchFeature('users.view', '*') // true - global wildcard
   * matchFeature('entities.records.view', 'entities.*') // true - module wildcard
   * matchFeature('entities', 'entities.*') // true - exact prefix match
   * matchFeature('users.view', 'entities.*') // false - different module
   * matchFeature('users.view', 'users.view') // true - exact match
   */
  matchFeature(required, granted) {
    return sharedMatchFeature(required, granted);
  }
  hasAllFeatures(required, granted) {
    return sharedHasAllFeatures(required, granted);
  }
  roleAclAllowsOrganization(acl, organizationId) {
    if (!organizationId) return true;
    const organizations = Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null;
    if (!organizations || !organizations.length || organizations.includes("__all__")) return true;
    return organizations.includes(organizationId);
  }
  getCacheKey(userId, scope) {
    return `rbac:${userId}:${scope.tenantId || "null"}:${scope.organizationId || "null"}`;
  }
  getUserTag(userId) {
    return `rbac:user:${userId}`;
  }
  getTenantTag(tenantId) {
    return `rbac:tenant:${tenantId}`;
  }
  getOrganizationTag(organizationId) {
    return `rbac:org:${organizationId}`;
  }
  async getFromCache(cacheKey) {
    if (!this.cache) return null;
    const cached = await this.cache.get(cacheKey);
    if (!cached) return null;
    return isAclData(cached) ? cached : null;
  }
  async setCache(cacheKey, data, userId, scope) {
    if (!this.cache) return;
    const tags = [
      this.getUserTag(userId),
      "rbac:all"
    ];
    if (scope.tenantId) {
      tags.push(this.getTenantTag(scope.tenantId));
    }
    if (scope.organizationId) {
      tags.push(this.getOrganizationTag(scope.organizationId));
    }
    await this.cache.set(cacheKey, data, {
      ttl: this.cacheTtlMs,
      tags
    });
  }
  /**
   * Invalidates cached ACL data for a specific user across all tenants and organizations.
   * Call this when a user's roles or user-specific ACL is modified.
   * 
   * @param userId - The ID of the user whose cache should be invalidated
   */
  async invalidateUserCache(userId) {
    this.globalSuperAdminCache.delete(userId);
    await this.deleteCacheByTags([this.getUserTag(userId), buildOrgScopeUserCacheTag(userId)]);
  }
  /**
   * Invalidates cached ACL data for all users within a specific tenant.
   * Call this when a role's ACL is modified, since roles are tenant-scoped
   * and affect all users in that tenant who have that role.
   * 
   * @param tenantId - The ID of the tenant whose cache should be invalidated
   */
  async invalidateTenantCache(tenantId) {
    this.globalSuperAdminCache.clear();
    await this.deleteCacheByTags([this.getTenantTag(tenantId), buildOrgScopeTenantCacheTag(tenantId)], [tenantId]);
  }
  /**
   * Invalidates cached ACL data for all users within a specific organization.
   * Call this when organization-level permissions or visibility changes.
   * 
   * @param organizationId - The ID of the organization whose cache should be invalidated
   */
  async invalidateOrganizationCache(organizationId) {
    await this.deleteCacheByTags([this.getOrganizationTag(organizationId)]);
  }
  /**
   * Clears all cached ACL data.
   * Use this for bulk operations or system-wide ACL changes.
   */
  async invalidateAllCache() {
    this.globalSuperAdminCache.clear();
    await this.deleteCacheByTags(["rbac:all"]);
  }
  async deleteCacheByTags(tags, tenantHints) {
    if (!this.cache) return;
    const contexts = /* @__PURE__ */ new Set();
    const current = getCurrentCacheTenant();
    contexts.add(current ?? null);
    contexts.add(null);
    if (Array.isArray(tenantHints)) {
      for (const hint of tenantHints) {
        contexts.add(hint ?? null);
      }
    }
    for (const ctx of contexts) {
      if (ctx === current) {
        await this.cache.deleteByTags(tags);
      } else {
        await runWithCacheTenant(ctx, async () => {
          await this.cache.deleteByTags(tags);
        });
      }
    }
  }
  async isGlobalSuperAdmin(userId) {
    if (this.globalSuperAdminCache.has(userId)) return this.globalSuperAdminCache.get(userId);
    const em = this.em.fork();
    const userSuper = await em.findOne(UserAcl, { user: userId, isSuperAdmin: true });
    if (userSuper && userSuper.isSuperAdmin) {
      this.globalSuperAdminCache.set(userId, true);
      return true;
    }
    const links = await findWithDecryption(
      em,
      UserRole,
      { user: userId },
      { populate: ["role"] },
      { tenantId: null, organizationId: null }
    );
    const linkList = Array.isArray(links) ? links : [];
    if (!linkList.length) {
      this.globalSuperAdminCache.set(userId, false);
      return false;
    }
    const roleIds = Array.from(new Set(linkList.map((link) => {
      const role = link.role;
      return role?.id ? String(role.id) : null;
    }).filter((id) => typeof id === "string" && id.length > 0)));
    if (!roleIds.length) {
      this.globalSuperAdminCache.set(userId, false);
      return false;
    }
    const roleSuper = await em.findOne(RoleAcl, { isSuperAdmin: true, role: { $in: roleIds } });
    const result = !!(roleSuper && roleSuper.isSuperAdmin);
    this.globalSuperAdminCache.set(userId, result);
    return result;
  }
  /**
   * Loads the Access Control List (ACL) for a user within a given scope.
   * 
   * The ACL resolution follows this priority:
   * 1. Per-user ACL (UserAcl) - if exists, use it exclusively
   * 2. Aggregated role ACLs (RoleAcl) - combine permissions from all user's roles
   * 
   * Results are cached for performance (default 5 minutes TTL).
   * Cache is automatically invalidated when ACL-related data changes.
   * 
   * @param userId - The ID of the user
   * @param scope - The tenant and organization context for ACL evaluation
   * @returns An object containing:
   *   - isSuperAdmin: If true, user has unrestricted access to all features
   *   - features: Array of feature strings (may include wildcards like 'entities.*')
   *   - organizations: Array of organization IDs user can access, or null for all organizations
   * 
   * @example
   * const acl = await rbacService.loadAcl('user-123', { tenantId: 'tenant-1', organizationId: 'org-1' })
   * // Returns: { isSuperAdmin: false, features: ['users.view', 'entities.*'], organizations: ['org-1', 'org-2'] }
   */
  async loadAcl(userId, scope) {
    const cacheKey = this.getCacheKey(userId, scope);
    const cached = await this.getFromCache(cacheKey);
    if (cached) return cached;
    if (!userId.startsWith("api_key:")) {
      if (await this.isGlobalSuperAdmin(userId)) {
        const result2 = { isSuperAdmin: true, features: ["*"], organizations: null };
        await this.setCache(cacheKey, result2, userId, scope);
        return result2;
      }
    }
    if (userId.startsWith("api_key:")) {
      const apiKeyId = userId.slice("api_key:".length);
      const em2 = this.em.fork();
      const key = await em2.findOne(ApiKey, { id: apiKeyId, deletedAt: null });
      if (!key || key.expiresAt && key.expiresAt.getTime() < Date.now()) {
        const result3 = { isSuperAdmin: false, features: [], organizations: null };
        await this.setCache(cacheKey, result3, userId, scope);
        return result3;
      }
      const tenantId2 = scope.tenantId || key.tenantId || null;
      const roleIds2 = Array.isArray(key.rolesJson) ? key.rolesJson.filter(Boolean) : [];
      let isSuper2 = false;
      const features2 = [];
      let organizations2 = key.organizationId ? [key.organizationId] : null;
      if (tenantId2 && roleIds2.length) {
        const racls = await em2.find(RoleAcl, { tenantId: tenantId2, role: { $in: roleIds2 } });
        for (const acl of racls) {
          isSuper2 = isSuper2 || !!acl.isSuperAdmin;
          if (Array.isArray(acl.featuresJson)) {
            for (const f of acl.featuresJson) if (!features2.includes(f)) features2.push(f);
          }
          if (organizations2 !== null) {
            if (acl.organizationsJson == null) {
              organizations2 = null;
            } else if (Array.isArray(acl.organizationsJson) && acl.organizationsJson.includes("__all__")) {
              organizations2 = null;
            } else {
              organizations2 = Array.from(/* @__PURE__ */ new Set([...organizations2 || [], ...acl.organizationsJson]));
            }
          }
        }
      }
      const result2 = { isSuperAdmin: isSuper2, features: features2, organizations: organizations2 };
      await this.setCache(cacheKey, result2, userId, scope);
      return result2;
    }
    const em = this.em.fork();
    const user = await em.findOne(User, { id: userId });
    if (!user) {
      const result2 = { isSuperAdmin: false, features: [], organizations: null };
      await this.setCache(cacheKey, result2, userId, scope);
      return result2;
    }
    const tenantId = scope.tenantId || user.tenantId || null;
    const orgId = scope.organizationId || user.organizationId || null;
    if (!tenantId) {
      const result2 = { isSuperAdmin: false, features: [], organizations: null };
      await this.setCache(cacheKey, result2, userId, scope);
      return result2;
    }
    const uacl = await em.findOne(UserAcl, { user: userId, tenantId });
    if (uacl) {
      const result2 = {
        isSuperAdmin: !!uacl.isSuperAdmin,
        features: Array.isArray(uacl.featuresJson) ? uacl.featuresJson : [],
        organizations: Array.isArray(uacl.organizationsJson) ? uacl.organizationsJson : null
      };
      await this.setCache(cacheKey, result2, userId, scope);
      return result2;
    }
    const links = await findWithDecryption(
      em,
      UserRole,
      { user: userId, role: { tenantId } },
      { populate: ["role"] },
      { tenantId, organizationId: orgId }
    );
    const linkList = Array.isArray(links) ? links : [];
    const roleIds = linkList.map((l) => l.role?.id).filter(Boolean);
    let isSuper = false;
    const features = [];
    let organizations = [];
    if (roleIds.length) {
      const racls = await em.find(RoleAcl, { tenantId, role: { $in: roleIds } }, {});
      const roleAcls = Array.isArray(racls) ? racls : [];
      for (const r of roleAcls) {
        isSuper = isSuper || !!r.isSuperAdmin;
        if (Array.isArray(r.featuresJson)) {
          for (const f of r.featuresJson) if (!features.includes(f)) features.push(f);
        }
        if (organizations !== null) {
          if (r.organizationsJson == null) organizations = null;
          else if (Array.isArray(r.organizationsJson) && r.organizationsJson.includes("__all__")) organizations = null;
          else organizations = Array.from(/* @__PURE__ */ new Set([...organizations || [], ...r.organizationsJson]));
        }
      }
    }
    if (organizations && orgId && !organizations.includes(orgId) && !organizations.includes("__all__")) {
    }
    const result = { isSuperAdmin: isSuper, features, organizations };
    await this.setCache(cacheKey, result, userId, scope);
    return result;
  }
  /**
   * Returns the user's granted feature strings for a given scope.
   *
   * Used by infrastructure that needs the raw grant list rather than a yes/no
   * authorization check (for example response enrichers gating themselves with
   * `features: [...]`). Callers MUST apply wildcard-aware matching against the
   * returned array — grants like `module.*` or `*` are part of the ACL contract.
   *
   * @param userId - The ID of the user
   * @param scope - The tenant and organization context for ACL evaluation
   * @returns Array of feature strings (may include wildcards); empty array when
   *          the user has no grants in scope
   */
  async getGrantedFeatures(userId, scope) {
    const acl = await this.loadAcl(userId, scope);
    return Array.isArray(acl.features) ? acl.features : [];
  }
  /**
   * Checks whether any tenant role grants a feature.
   *
   * This supports non-user runtimes such as scheduler workers that execute with
   * tenant scope but without an authenticated user.
   */
  async tenantHasFeature(tenantId, feature, opts) {
    if (!tenantId || !feature) return false;
    const enabledIds = getEnabledModuleIds();
    if (enabledIds.length && !enabledIds.includes(getOwningModuleId(feature))) return false;
    const em = this.em.fork();
    const roleAcls = await em.find(RoleAcl, { tenantId, deletedAt: null }, {});
    const list = Array.isArray(roleAcls) ? roleAcls : [];
    const organizationId = opts?.organizationId ?? null;
    for (const acl of list) {
      if (!this.roleAclAllowsOrganization(acl, organizationId)) continue;
      if (acl.isSuperAdmin) return true;
      const grants = Array.isArray(acl.featuresJson) ? acl.featuresJson : [];
      if (this.hasAllFeatures([feature], filterGrantsByEnabledModules(grants))) return true;
    }
    return false;
  }
  /**
   * Checks if a user has all required features within a given scope.
   *
   * This is the primary authorization check method used throughout the application.
   * It combines feature checking with organization visibility validation.
   *
   * Authorization logic:
   * 1. No features required → always returns true
   * 2. User is super admin → always returns true
   * 3. Organization restriction check: If the user's ACL has a restricted organization list
   *    and the requested organization is not in that list → returns false
   * 4. Feature matching: User must have all required features (supports wildcards)
   *
   * @param userId - The ID of the user
   * @param required - Array of feature strings to check (e.g., ['users.view', 'users.edit'])
   * @param scope - The tenant and organization context for authorization
   * @returns true if the user has all required features and organization access, false otherwise
   *
   * @example
   * // Check if user can view and edit users
   * const canManageUsers = await rbacService.userHasAllFeatures(
   *   'user-123',
   *   ['users.view', 'users.edit'],
   *   { tenantId: 'tenant-1', organizationId: 'org-1' }
   * )
   *
   * @example
   * // Check with wildcard features
   * const canAccessEntities = await rbacService.userHasAllFeatures(
   *   'user-123',
   *   ['entities.records.view'],
   *   { tenantId: 'tenant-1', organizationId: 'org-1' }
   * )
   * // Returns true if user has 'entities.*', '*', or 'entities.records.view'
   */
  async userHasAllFeatures(userId, required, scope) {
    if (!required.length) return true;
    const acl = await this.loadAcl(userId, scope);
    if (acl.isSuperAdmin) {
      const enabledIds = getEnabledModuleIds();
      if (!enabledIds.length) return true;
      const enabledSet = new Set(enabledIds);
      return required.every((feature) => enabledSet.has(getOwningModuleId(feature)));
    }
    if (acl.organizations && scope.organizationId && !acl.organizations.includes(scope.organizationId) && !acl.organizations.includes("__all__")) return false;
    return this.hasAllFeatures(required, filterGrantsByEnabledModules(acl.features));
  }
}
export {
  RbacService
};
//# sourceMappingURL=rbacService.js.map
