var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { BaseEngine, LogError, LogStatusEx, PermissionDeniedError, PermissionProviderBase, RegisterForStartup, RunView, } from '@memberjunction/core';
import { MJGlobal } from '@memberjunction/global';
/**
 * The permission-backing entity names we aggregate in `GetAuditTimeline`. Each entry
 * pairs a `PermissionDomain.Name` with the physical entity whose RecordChange rows
 * describe audit events. New providers added after Phase 2b should register here too.
 */
const DOMAIN_TO_AUDIT_ENTITIES = {
    'Entity Permissions': ['MJ: Entity Permissions'],
    'Application Roles': ['MJ: Application Roles'],
    'Dashboard Permissions': ['MJ: Dashboard Permissions', 'MJ: Dashboard Category Permissions'],
    'Resource Permissions': ['MJ: Resource Permissions'],
    'Artifact Permissions': ['MJ: Artifact Permissions'],
    'Collection Permissions': ['MJ: Collection Permissions'],
    'AI Agent Permissions': ['MJ: AI Agent Permissions'],
    'AI Skill Permissions': ['MJ: AI Skill Permissions'],
    'Query Permissions': ['MJ: Query Permissions'],
    'Access Control Rules': ['MJ: Access Control Rules'],
};
/**
 * Canonical Font Awesome class for each permission domain. Exported so the admin
 * Permissions app and the end-user Sharing Center render the same glyph for each
 * domain without drifting. Consumers should call
 * {@link PermissionEngine.DomainIconFor} rather than reading the map directly so
 * unknown domains get the lock fallback.
 */
export const PERMISSION_DOMAIN_ICONS = {
    'Entity Permissions': 'fa-solid fa-database',
    'Application Roles': 'fa-solid fa-shield-halved',
    'Dashboard Permissions': 'fa-solid fa-chart-line',
    'Resource Permissions': 'fa-solid fa-share-nodes',
    'Artifact Permissions': 'fa-solid fa-file-lines',
    'Collection Permissions': 'fa-solid fa-folder-open',
    'AI Agent Permissions': 'fa-solid fa-robot',
    'AI Skill Permissions': 'fa-solid fa-wand-magic-sparkles',
    'Query Permissions': 'fa-solid fa-magnifying-glass',
    'Access Control Rules': 'fa-solid fa-lock',
};
/** Fallback icon used when a domain isn't in the catalog (e.g., a Phase-3 provider). */
export const PERMISSION_DOMAIN_ICON_FALLBACK = 'fa-solid fa-lock';
/**
 * PermissionEngine is the unified entry point for permission queries across every registered
 * permission subsystem. At startup it loads the {@link MJPermissionDomainEntity} catalog, then
 * instantiates each active domain's provider via ClassFactory (keyed on `ProviderClassName`).
 *
 * Typical usage:
 * ```typescript
 * // "Can this user read entity X?"
 * const result = PermissionEngine.Instance.CheckPermission(
 *     user, 'Entity Permissions', 'Users', null, 'Read'
 * );
 *
 * // "Show me everything this user has access to"
 * const all = PermissionEngine.Instance.GetAllUserPermissions(user);
 *
 * // "Who has access to this resource?"
 * const rows = PermissionEngine.Instance.GetResourcePermissions(
 *     'Dashboard Permissions', 'Dashboards', dashboardId
 * );
 * ```
 *
 * Priority 15 so it configures after default-priority engines (e.g., UserInfoEngine at 10);
 * providers lazy-resolve their backing engines on first use, so even if a backing engine
 * has not been configured yet at provider construction time, permission checks still work
 * once the backing engine comes online.
 */
let PermissionEngine = class PermissionEngine extends BaseEngine {
    constructor() {
        super(...arguments);
        this._domains = [];
        this._providers = new Map();
    }
    static get Instance() {
        return super.getInstance();
    }
    async Config(forceRefresh, contextUser, provider) {
        const configs = [
            {
                Type: 'entity',
                EntityName: 'MJ: Permission Domains',
                PropertyName: '_domains',
                CacheLocal: true,
            },
        ];
        await super.Load(configs, provider, forceRefresh, contextUser);
        this.instantiateProviders();
    }
    /**
     * Walks the loaded domain catalog and instantiates each active provider via ClassFactory.
     * Called on every `Config()` so a `forceRefresh` re-wires the provider map.
     */
    instantiateProviders() {
        this._providers.clear();
        const activeDomains = this._domains.filter((d) => d.IsActive);
        for (const domain of activeDomains) {
            try {
                const instance = MJGlobal.Instance.ClassFactory.CreateInstance(PermissionProviderBase, domain.ProviderClassName);
                if (instance) {
                    this._providers.set(domain.Name, instance);
                }
                else {
                    LogError(`PermissionEngine: no provider registered for ProviderClassName '${domain.ProviderClassName}' ` +
                        `(domain '${domain.Name}'). Domain will be skipped until a provider is registered.`);
                }
            }
            catch (err) {
                LogError(`PermissionEngine: failed to instantiate provider '${domain.ProviderClassName}': ${err}`);
            }
        }
        LogStatusEx({
            message: `PermissionEngine configured with ${this._providers.size} provider(s) ` +
                `out of ${activeDomains.length} active domain(s).`,
            verboseOnly: true,
        });
    }
    /**
     * Check whether a user has a specific permission on a specific resource within a domain.
     * Returns `{Allowed: false, Reason: 'Unknown domain'}` if the domain is not registered
     * (either because no catalog row exists or because its provider failed to load).
     */
    async CheckPermission(user, domainName, resourceType, resourceId, action) {
        const provider = this._providers.get(domainName);
        if (!provider) {
            return {
                Allowed: false,
                DomainName: domainName,
                Reason: `Unknown permission domain '${domainName}'`,
            };
        }
        return provider.CheckPermission(user, resourceType, resourceId, action);
    }
    /**
     * Aggregate every resource the user has access to across all registered providers.
     * Providers are queried in parallel; failures from individual providers are logged and skipped.
     * Powers the Sharing Center "User Access Report" view.
     */
    async GetAllUserPermissions(user) {
        const providers = Array.from(this._providers.values());
        const settled = await Promise.allSettled(providers.map((p) => p.GetUserResources(user)));
        const results = [];
        settled.forEach((outcome, i) => {
            const provider = providers[i];
            if (outcome.status === 'fulfilled') {
                if (outcome.value?.length)
                    results.push(...outcome.value);
            }
            else {
                LogError(`PermissionEngine.GetAllUserPermissions: provider '${provider.DomainName}' threw: ${outcome.reason}`);
            }
        });
        return results;
    }
    /**
     * Aggregate every permission this user has granted to *other* users across all
     * registered providers. Powers the end-user Sharing Center's "Shared by me" tab.
     *
     * Role-only providers (Entity Permissions, Application Roles, AI Agent Permissions,
     * Query Permissions) contribute nothing — their base-class default returns `[]`.
     * Providers are queried in parallel; failures are logged and skipped.
     */
    async GetPermissionsGrantedByUser(grantor) {
        const providers = Array.from(this._providers.values());
        const settled = await Promise.allSettled(providers.map((p) => p.GetPermissionsGrantedByUser(grantor)));
        const results = [];
        settled.forEach((outcome, i) => {
            const provider = providers[i];
            if (outcome.status === 'fulfilled') {
                if (outcome.value?.length)
                    results.push(...outcome.value);
            }
            else {
                LogError(`PermissionEngine.GetPermissionsGrantedByUser: provider '${provider.DomainName}' threw: ${outcome.reason}`);
            }
        });
        return results;
    }
    /**
     * Aggregate every permission where this user is the grantee AND someone else is the
     * grantor — the "shared with me" view. Excludes self-owned resources and role-inherited
     * access. Providers are queried in parallel; failures are logged and skipped.
     */
    async GetPermissionsSharedWithUser(grantee) {
        const providers = Array.from(this._providers.values());
        const settled = await Promise.allSettled(providers.map((p) => p.GetPermissionsSharedWithUser(grantee)));
        const results = [];
        settled.forEach((outcome, i) => {
            const provider = providers[i];
            if (outcome.status === 'fulfilled') {
                if (outcome.value?.length)
                    results.push(...outcome.value);
            }
            else {
                LogError(`PermissionEngine.GetPermissionsSharedWithUser: provider '${provider.DomainName}' threw: ${outcome.reason}`);
            }
        });
        return results;
    }
    /**
     * Get every grantee's permissions on a specific resource within a domain.
     * Powers the Sharing Center "Resource Access Report" view.
     */
    async GetResourcePermissions(domainName, resourceType, resourceId) {
        const provider = this._providers.get(domainName);
        if (!provider)
            return [];
        return provider.GetResourcePermissions(resourceType, resourceId);
    }
    /** All active, loaded domain catalog records. */
    get Domains() {
        return this.GetConfigData('_domains').filter((d) => d.IsActive);
    }
    /** Look up the provider for a specific domain by name; returns undefined when not loaded. */
    GetProvider(domainName) {
        return this._providers.get(domainName);
    }
    /**
     * The resource type names supported by the given permission domain — sourced
     * from the provider's `GetResourceTypes()`. Returns `[]` when the domain is
     * unknown or the provider doesn't enumerate its types. Used by admin UIs
     * (e.g., the Resource Access Report) to populate the Resource Type picker
     * after a Domain is selected.
     */
    GetResourceTypes(domainName, provider) {
        return this._providers.get(domainName)?.GetResourceTypes(provider ?? this.ProviderToUse) ?? [];
    }
    /**
     * Font Awesome class for a permission domain, with a lock fallback for
     * unknown domains. Drives the icon shown next to domain groups in both the
     * admin Permissions app and the end-user Sharing Center.
     */
    static DomainIconFor(domainName) {
        return PERMISSION_DOMAIN_ICONS[domainName] ?? PERMISSION_DOMAIN_ICON_FALLBACK;
    }
    /** For unit tests: swap the provider map with a caller-supplied one. */
    _SetProvidersForTesting(providers) {
        this._providers = providers;
    }
    /**
     * Convenience wrapper around {@link CheckPermission} that throws a typed
     * {@link PermissionDeniedError} when the action is not permitted. Intended for
     * programmatic callers — Actions, AI Agents, custom server code — that want
     * "fail loudly on denial" semantics instead of branching on a boolean result.
     *
     * @example
     * ```typescript
     * await PermissionEngine.Instance.AuthorizeOrThrow(
     *     user, 'Dashboard Permissions', 'Dashboards', dashboardId, 'Share'
     * );
     * // If we reach this line, the user can share the dashboard.
     * ```
     *
     * @throws {@link PermissionDeniedError} when the provider returns `Allowed: false`
     *         or when the domain is not registered.
     */
    async AuthorizeOrThrow(user, domainName, resourceType, resourceId, action) {
        const result = await this.CheckPermission(user, domainName, resourceType, resourceId, action);
        if (!result.Allowed) {
            throw new PermissionDeniedError({ ...result, ResourceType: resourceType, ResourceID: resourceId, Action: action });
        }
    }
    /**
     * Query RecordChange for audit events against every permission-backing entity.
     * Supports filtering by domain, date range, changed-by user, or a specific
     * record. Results are sorted newest-first and capped at `filter.MaxRows`
     * (default 500).
     *
     * Relies on each permission entity having `TrackRecordChanges=1` in its
     * Entity metadata row. Domains whose backing entities do not track changes
     * contribute nothing to the timeline.
     */
    async GetAuditTimeline(filter = {}) {
        const targetEntities = this.resolveAuditEntityNames(filter.DomainName);
        if (targetEntities.length === 0)
            return [];
        const clauses = [
            `Entity IN (${targetEntities.map((n) => `'${n.replace(/'/g, "''")}'`).join(',')})`,
        ];
        if (filter.StartDate)
            clauses.push(`ChangedAt >= '${filter.StartDate.toISOString()}'`);
        if (filter.EndDate)
            clauses.push(`ChangedAt <= '${filter.EndDate.toISOString()}'`);
        if (filter.ChangedByUserID)
            clauses.push(`UserID = '${filter.ChangedByUserID}'`);
        if (filter.RecordID)
            clauses.push(`RecordID = '${filter.RecordID.replace(/'/g, "''")}'`);
        const maxRows = filter.MaxRows ?? 500;
        const rv = new RunView();
        const result = await rv.RunView({
            EntityName: 'MJ: Record Changes',
            ExtraFilter: clauses.join(' AND '),
            OrderBy: 'ChangedAt DESC',
            MaxRows: maxRows,
            Fields: ['ID', 'Entity', 'RecordID', 'UserID', 'User', 'Type', 'ChangedAt', 'ChangesDescription'],
            ResultType: 'simple',
        });
        if (!result.Success) {
            LogError(`PermissionEngine.GetAuditTimeline: ${result.ErrorMessage}`);
            return [];
        }
        return (result.Results ?? []).map((row) => ({
            ChangedAt: new Date(row.ChangedAt),
            ChangedByUserID: row.UserID,
            ChangedByUserName: row.User ?? undefined,
            DomainName: this.resolveDomainForEntity(row.Entity),
            EntityName: row.Entity,
            RecordID: row.RecordID,
            ChangeType: row.Type,
            Summary: row.ChangesDescription ?? undefined,
            SourceRecordChangeID: row.ID,
        }));
    }
    /**
     * Map a domain-name filter to the set of backing entity names to query.
     * When no filter is given, every known permission entity is included.
     */
    resolveAuditEntityNames(domainName) {
        if (domainName)
            return DOMAIN_TO_AUDIT_ENTITIES[domainName] ?? [];
        const all = [];
        for (const list of Object.values(DOMAIN_TO_AUDIT_ENTITIES)) {
            all.push(...list);
        }
        return all;
    }
    /**
     * Reverse lookup: given a backing entity name, return the owning domain.
     * Falls back to the entity name itself when we can't resolve (e.g., a
     * caller added a new permission entity to the map but no provider yet).
     */
    resolveDomainForEntity(entityName) {
        for (const [domain, entities] of Object.entries(DOMAIN_TO_AUDIT_ENTITIES)) {
            if (entities.includes(entityName))
                return domain;
        }
        return entityName;
    }
};
PermissionEngine = __decorate([
    RegisterForStartup({ priority: 15, severity: 'warn', description: 'PermissionEngine — unified permission provider registry' })
], PermissionEngine);
export { PermissionEngine };
//# sourceMappingURL=PermissionEngine.js.map