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, LogStatus, RegisterForStartup, RunView, } from '@memberjunction/core';
import { UUIDsEqual } from '@memberjunction/global';
/**
 * UserInfoEngine is a singleton engine that provides centralized access to user-specific data
 * including notifications, workspaces, applications, favorites, and record logs.
 *
 * This engine consolidates multiple user-related RunView calls into a single batched load,
 * improving performance and enabling local caching for faster subsequent access.
 *
 * Usage:
 * ```typescript
 * // Get the instance (auto-configured on startup)
 * const engine = UserInfoEngine.Instance;
 *
 * // Access user data
 * const notifications = engine.UserNotifications;
 * const workspace = engine.CurrentWorkspace;
 * const apps = engine.UserApplications;
 * ```
 *
 * Note: This engine filters all data by the current user's ID automatically.
 * On the server side, you must call Config() with contextUser to specify which user's data to load.
 */
let UserInfoEngine = class UserInfoEngine extends BaseEngine {
    constructor() {
        super(...arguments);
        // Private storage for entity data
        this._UserNotifications = [];
        this._Workspaces = [];
        this._UserApplications = [];
        this._UserFavorites = [];
        this._UserRecordLogs = [];
        this._UserSettings = [];
        // Notification types (global - not user-specific)
        this._NotificationTypes = [];
        // User notification preferences (user-specific)
        this._UserNotificationPreferences = [];
        // Application role assignments (global - not user-specific)
        this._applicationRoles = [];
        // Optional filters for customizing application access/admin checks (e.g., tenant-scoped roles)
        this._applicationAccessFilter = null;
        this._applicationAdminFilter = null;
        // Track the user ID we loaded data for
        this._loadedForUserId = null;
        // Track in-progress CreateDefaultApplications call to prevent duplicate execution
        this._createDefaultAppsPromise = null;
        // ========================================================================
        // DEBOUNCED SETTINGS SUPPORT
        // ========================================================================
        /**
         * Debounce time in milliseconds for SetSettingDebounced calls.
         * Default is 500ms. Change via the SettingsDebounceMs setter.
         */
        this._settingsDebounceMs = 500;
        /**
         * Map of pending setting updates: key -> { value, contextUser, timestamp }
         * These are queued and flushed after the debounce period of inactivity.
         */
        this._pendingSettings = new Map();
        /**
         * Timer handle for the debounce flush
         */
        this._settingsDebounceTimer = null;
        /**
         * Promise that resolves when the current flush operation completes.
         * Used to prevent concurrent flush operations.
         */
        this._flushPromise = null;
    }
    /**
     * Returns the global instance of the class. This is a singleton class, so there is only one instance of it in the application.
     * Do not directly create new instances of it, always use this method to get the instance.
     */
    static get Instance() {
        return super.getInstance();
    }
    /**
     * Configures the engine by loading user-specific metadata from the database.
     * All entities are filtered by the current user's ID and cached locally for performance.
     *
     * @param forceRefresh - If true, forces a refresh from the server even if data is cached
     * @param contextUser - The user context (required for server-side, auto-detected on client)
     * @param provider - Optional custom metadata provider
     */
    async Config(forceRefresh, contextUser, provider) {
        const md = provider ?? this.ProviderToUse;
        const userId = contextUser?.ID || md.CurrentUser?.ID;
        if (!userId) {
            console.warn('UserInfoEngine: No user context available, skipping configuration');
            return;
        }
        // Check if we need to reload due to user change
        if (this._loadedForUserId && this._loadedForUserId !== userId) {
            forceRefresh = true; // Force refresh if user changed
        }
        // On the client (Network/GraphQL provider), filter user-specific entities by UserID
        // to avoid loading all users' data. On the server (Database provider), load everything
        // because the server handles multiple users from a single process.
        const isClientSide = this.ProviderToUse.ProviderType === 'Network';
        const userFilter = isClientSide && userId ? `UserID='${userId}'` : undefined;
        const configs = [
            {
                Type: 'entity',
                EntityName: 'MJ: User Notifications',
                PropertyName: '_UserNotifications',
                CacheLocal: true,
                Filter: userFilter,
            },
            {
                Type: 'entity',
                EntityName: 'MJ: User Notification Types',
                PropertyName: '_NotificationTypes',
                CacheLocal: true,
                // Global reference table — no user filter
            },
            {
                Type: 'entity',
                EntityName: 'MJ: Workspaces',
                PropertyName: '_Workspaces',
                CacheLocal: true,
                Filter: userFilter,
            },
            {
                Type: 'entity',
                EntityName: 'MJ: User Settings',
                PropertyName: '_UserSettings',
                CacheLocal: true,
                Filter: userFilter,
            },
            {
                Type: 'entity',
                EntityName: 'MJ: User Applications',
                PropertyName: '_UserApplications',
                CacheLocal: true,
                Filter: userFilter,
            },
            {
                Type: 'entity',
                EntityName: 'MJ: User Favorites',
                PropertyName: '_UserFavorites',
                CacheLocal: true,
                Filter: userFilter,
            },
            {
                Type: 'entity',
                EntityName: 'MJ: User Record Logs',
                PropertyName: '_UserRecordLogs',
                CacheLocal: true,
                Filter: userFilter,
            },
            {
                Type: 'entity',
                EntityName: 'MJ: User Notification Preferences',
                PropertyName: '_UserNotificationPreferences',
                CacheLocal: true,
                Filter: userFilter,
            },
            {
                Type: 'entity',
                EntityName: 'MJ: Application Roles',
                PropertyName: '_applicationRoles',
                CacheLocal: true,
                // Global reference table — no user filter
            },
        ];
        await super.Load(configs, provider, forceRefresh, contextUser);
        this._loadedForUserId = userId;
    }
    // ========================================================================
    // OBSERVABLE ACCESSORS
    // ========================================================================
    /**
     * Observable stream of the notifications cache array. Emits the current array on subscribe
     * and re-emits whenever the cache is mutated (save, delete, remote-invalidate, refresh).
     *
     * Emits the raw unfiltered cache (all users). Consumers that need per-user filtering should
     * `pipe(map(...))` — the public {@link UserNotifications} getter applies the current-user filter.
     */
    get UserNotifications$() {
        return this.ObserveProperty('_UserNotifications');
    }
    /**
     * Observable stream of the user favorites cache array. Emits the current array on subscribe
     * and re-emits whenever the cache is mutated.
     */
    get UserFavorites$() {
        return this.ObserveProperty('_UserFavorites');
    }
    /**
     * Observable stream of the user applications cache array. Emits the current array on subscribe
     * and re-emits whenever the cache is mutated.
     */
    get UserApplications$() {
        return this.ObserveProperty('_UserApplications');
    }
    // ========================================================================
    // PUBLIC ACCESSORS
    // ========================================================================
    /**
     * Get all notifications for the current user, ordered by creation date (newest first)
     */
    get UserNotifications() {
        if (!this._loadedForUserId)
            return [];
        return this.GetConfigData('_UserNotifications')
            .filter((n) => UUIDsEqual(n.UserID, this._loadedForUserId))
            .sort((a, b) => new Date(b.Get('__mj_CreatedAt')).getTime() - new Date(a.Get('__mj_CreatedAt')).getTime());
    }
    /**
     * Get all settings for the current user
     */
    get UserSettings() {
        if (!this._loadedForUserId)
            return [];
        return this.GetConfigData('_UserSettings')
            .filter((s) => UUIDsEqual(s.UserID, this._loadedForUserId));
    }
    /**
     * Get a user setting value by key.
     *
     * **Read-after-write semantics**: this consults the in-memory pending-
     * debounced-writes map FIRST. Without that, a `SetSettingDebounced`
     * followed immediately by `GetSetting` would return the old DB-cached
     * value because the debounce timer hasn't fired yet and the entity
     * hasn't been saved. Callers that update a preference and re-render
     * synchronously (e.g. form-variant picker → form reload) depend on
     * this freshness guarantee.
     *
     * @param settingKey - The setting key to find (e.g., "default-view-setting/Contacts")
     * @returns The setting value string, or undefined if not found
     */
    GetSetting(settingKey) {
        const pending = this._pendingSettings.get(settingKey);
        if (pending !== undefined)
            return pending.value;
        const setting = this.UserSettings.find((s) => s.Setting === settingKey);
        return setting?.Value ?? undefined;
    }
    /**
     * Get a user setting entity by key.
     * @param settingKey - The setting key to find
     * @returns The MJUserSettingEntity, or undefined if not found
     */
    GetSettingEntity(settingKey) {
        return this.UserSettings.find((s) => s.Setting === settingKey);
    }
    /**
     * Set a user setting by key. Creates a new setting if it doesn't exist.
     * @param settingKey - The setting key (e.g., "default-view-setting/Contacts")
     * @param value - The setting value (string, typically JSON for complex data)
     * @param contextUser - Optional user context for server-side use
     * @returns true if successful, false otherwise
     */
    async SetSetting(settingKey, value, contextUser) {
        const md = this.ProviderToUse;
        const userId = contextUser?.ID || md.CurrentUser?.ID;
        if (!userId) {
            console.error('UserInfoEngine.SetSetting: No user context available');
            return false;
        }
        try {
            // Try to find existing setting
            let setting = this.GetSettingEntity(settingKey);
            if (setting) {
                // Update existing setting
                setting.Value = value;
            }
            else {
                // Create new setting
                setting = await md.GetEntityObject('MJ: User Settings', contextUser);
                setting.NewRecord();
                setting.UserID = userId;
                setting.Setting = settingKey;
                setting.Value = value;
            }
            const saved = await setting.Save();
            if (saved) {
                // If it was a new record, add to cache
                if (!this._UserSettings.some((s) => UUIDsEqual(s.ID, setting.ID))) {
                    this._UserSettings.push(setting);
                }
                return true;
            }
            else {
                console.error('UserInfoEngine.SetSetting: Failed to save:', setting.LatestResult?.Message);
                return false;
            }
        }
        catch (error) {
            console.error('UserInfoEngine.SetSetting: Error:', error instanceof Error ? error.message : String(error));
            return false;
        }
    }
    /**
     * Delete a user setting by key.
     * @param settingKey - The setting key to delete
     * @returns true if successful (or setting didn't exist), false on error
     */
    async DeleteSetting(settingKey) {
        const setting = this.GetSettingEntity(settingKey);
        if (!setting) {
            // Setting doesn't exist, consider it a success
            return true;
        }
        try {
            const deleted = await setting.Delete();
            if (deleted) {
                // Remove from cache
                const index = this._UserSettings.findIndex((s) => UUIDsEqual(s.ID, setting.ID));
                if (index >= 0) {
                    this._UserSettings.splice(index, 1);
                }
                return true;
            }
            else {
                console.error('UserInfoEngine.DeleteSetting: Failed to delete:', setting.LatestResult?.Message);
                return false;
            }
        }
        catch (error) {
            console.error('UserInfoEngine.DeleteSetting: Error:', error instanceof Error ? error.message : String(error));
            return false;
        }
    }
    // ========================================================================
    // DEBOUNCED SETTINGS METHODS
    // ========================================================================
    /**
     * Get the current debounce time in milliseconds for SetSettingDebounced calls.
     */
    get SettingsDebounceMs() {
        return this._settingsDebounceMs;
    }
    /**
     * Set the debounce time in milliseconds for SetSettingDebounced calls.
     * When changed, any pending settings are flushed first before the new debounce time takes effect.
     * @param value - The debounce time in milliseconds (minimum 100ms, maximum 10000ms)
     */
    set SettingsDebounceMs(value) {
        const clampedValue = Math.max(100, Math.min(10000, value));
        if (clampedValue !== this._settingsDebounceMs) {
            // Flush any pending settings before changing the debounce time
            this.FlushPendingSettings();
            this._settingsDebounceMs = clampedValue;
        }
    }
    /**
     * Queue a setting update with debouncing. Multiple calls within the debounce period
     * are batched together, with only the last value for each key being saved.
     * The actual database save occurs after the debounce period of inactivity.
     *
     * This is the preferred method for UI components that may update settings frequently
     * (e.g., on every resize, scroll, or input change).
     *
     * @param settingKey - The setting key (e.g., "AI_DASHBOARD_ROOT/ai-models")
     * @param value - The setting value (string, typically JSON for complex data)
     * @param contextUser - Optional user context for server-side use
     */
    SetSettingDebounced(settingKey, value, contextUser) {
        // Queue the setting update
        this._pendingSettings.set(settingKey, {
            value,
            contextUser,
            timestamp: Date.now(),
        });
        // Reset the debounce timer
        if (this._settingsDebounceTimer) {
            clearTimeout(this._settingsDebounceTimer);
        }
        this._settingsDebounceTimer = setTimeout(() => {
            this.FlushPendingSettings();
        }, this._settingsDebounceMs);
    }
    /**
     * Immediately flush all pending debounced settings to the database.
     * Call this when you need to ensure settings are saved (e.g., before navigation).
     * Safe to call multiple times - concurrent calls will wait for the current flush to complete.
     *
     * @returns Promise that resolves when all pending settings have been saved
     */
    async FlushPendingSettings() {
        // Clear the timer since we're flushing now
        if (this._settingsDebounceTimer) {
            clearTimeout(this._settingsDebounceTimer);
            this._settingsDebounceTimer = null;
        }
        // If nothing pending, return immediately
        if (this._pendingSettings.size === 0) {
            return;
        }
        // If a flush is already in progress, wait for it and then check again
        if (this._flushPromise) {
            await this._flushPromise;
            // After waiting, there might be new pending settings, so recurse
            if (this._pendingSettings.size > 0) {
                return this.FlushPendingSettings();
            }
            return;
        }
        // Take a snapshot of pending settings and clear the queue
        const settingsToSave = new Map(this._pendingSettings);
        this._pendingSettings.clear();
        // Create the flush promise
        this._flushPromise = this.doFlushSettings(settingsToSave);
        try {
            await this._flushPromise;
        }
        finally {
            this._flushPromise = null;
        }
    }
    /**
     * Internal method to perform the actual flush of settings.
     * @param settingsToSave - Map of settings to save
     */
    async doFlushSettings(settingsToSave) {
        const savePromises = [];
        for (const [key, { value, contextUser }] of settingsToSave) {
            savePromises.push(this.SetSetting(key, value, contextUser));
        }
        const results = await Promise.all(savePromises);
        const failedCount = results.filter((r) => !r).length;
        if (failedCount > 0) {
            console.warn(`UserInfoEngine.FlushPendingSettings: ${failedCount} of ${results.length} settings failed to save`);
        }
    }
    /**
     * Check if there are any pending debounced settings waiting to be saved.
     */
    get HasPendingSettings() {
        return this._pendingSettings.size > 0;
    }
    /**
     * Get the number of pending debounced settings.
     */
    get PendingSettingsCount() {
        return this._pendingSettings.size;
    }
    /**
     * Get unread notifications for the current user
     */
    get UnreadNotifications() {
        return this.UserNotifications.filter((n) => n.Unread);
    }
    /**
     * Get the count of unread notifications
     */
    get UnreadNotificationCount() {
        return this.UnreadNotifications.length;
    }
    /**
     * Get all workspaces for the current user
     */
    get Workspaces() {
        if (!this._loadedForUserId)
            return [];
        return this.GetConfigData('_Workspaces')
            .filter((w) => UUIDsEqual(w.UserID, this._loadedForUserId));
    }
    /**
     * Get the current user's primary workspace (first one if multiple exist)
     */
    get CurrentWorkspace() {
        const workspaces = this.Workspaces;
        return workspaces.length > 0 ? workspaces[0] : null;
    }
    /**
     * Get all applications enabled for the current user, ordered by sequence then application name
     */
    get UserApplications() {
        if (!this._loadedForUserId)
            return [];
        return this.GetConfigData('_UserApplications')
            .filter((ua) => UUIDsEqual(ua.UserID, this._loadedForUserId))
            .sort((a, b) => {
            // Sort by Sequence first, then by Application name
            if (a.Sequence !== b.Sequence) {
                return a.Sequence - b.Sequence;
            }
            return (a.Application || '').localeCompare(b.Application || '');
        });
    }
    /**
     * Get all favorites for the current user, ordered by creation date (newest first)
     */
    get UserFavorites() {
        if (!this._loadedForUserId)
            return [];
        return this.GetConfigData('_UserFavorites')
            .filter((f) => UUIDsEqual(f.UserID, this._loadedForUserId))
            .sort((a, b) => new Date(b.Get('__mj_CreatedAt')).getTime() - new Date(a.Get('__mj_CreatedAt')).getTime());
    }
    /**
     * Get all record logs for the current user (recent record access), ordered by LatestAt (most recent first)
     */
    get UserRecordLogs() {
        if (!this._loadedForUserId)
            return [];
        return this.GetConfigData('_UserRecordLogs')
            .filter((r) => UUIDsEqual(r.UserID, this._loadedForUserId))
            .sort((a, b) => new Date(b.LatestAt).getTime() - new Date(a.LatestAt).getTime());
    }
    // ========================================================================
    // RAW DATA ACCESSORS (unfiltered, for admin/server scenarios)
    // ========================================================================
    /**
     * Get ALL notifications in the cache (unfiltered by user).
     * Useful for server-side admin scenarios.
     */
    get AllNotifications() {
        return this.GetConfigData('_UserNotifications');
    }
    /**
     * Get ALL user applications in the cache (unfiltered by user).
     * Useful for server-side admin scenarios.
     */
    get AllUserApplications() {
        return this.GetConfigData('_UserApplications');
    }
    /**
     * Get notifications for a specific user
     * @param userId - The user ID to filter by
     */
    GetNotificationsForUser(userId) {
        return (this._UserNotifications || [])
            .filter((n) => UUIDsEqual(n.UserID, userId))
            .sort((a, b) => new Date(b.Get('__mj_CreatedAt')).getTime() - new Date(a.Get('__mj_CreatedAt')).getTime());
    }
    /**
     * Get user applications for a specific user
     * @param userId - The user ID to filter by
     */
    GetUserApplicationsForUser(userId) {
        return (this._UserApplications || [])
            .filter((ua) => UUIDsEqual(ua.UserID, userId))
            .sort((a, b) => {
            if (a.Sequence !== b.Sequence) {
                return a.Sequence - b.Sequence;
            }
            return (a.Application || '').localeCompare(b.Application || '');
        });
    }
    // ========================================================================
    // UTILITY METHODS
    // ========================================================================
    /**
     * Get a notification by ID
     * @param notificationId - The notification ID to find
     */
    GetNotificationById(notificationId) {
        return this.UserNotifications.find((n) => UUIDsEqual(n.ID, notificationId));
    }
    /**
     * Get a user application by application ID
     * @param applicationId - The application ID to find
     */
    GetUserApplicationByAppId(applicationId) {
        return this.UserApplications.find((ua) => UUIDsEqual(ua.ApplicationID, applicationId));
    }
    /**
     * Get favorites for a specific entity
     * @param entityId - The entity ID to filter by
     */
    GetFavoritesForEntity(entityId) {
        return this.UserFavorites.filter((f) => UUIDsEqual(f.EntityID, entityId));
    }
    /**
     * Get recent record logs for a specific entity
     * @param entityId - The entity ID to filter by
     * @param maxItems - Maximum number of items to return (default: 10)
     */
    GetRecentRecordsForEntity(entityId, maxItems = 10) {
        return this.UserRecordLogs.filter((r) => UUIDsEqual(r.EntityID, entityId)).slice(0, maxItems);
    }
    /**
     * Check if a record is in the user's favorites
     * @param entityId - The entity ID
     * @param recordId - The record ID
     */
    IsRecordFavorite(entityId, recordId) {
        return this.UserFavorites.some((f) => UUIDsEqual(f.EntityID, entityId) && UUIDsEqual(f.RecordID, recordId));
    }
    /**
     * Get the user ID this engine was loaded for
     */
    get LoadedForUserId() {
        return this._loadedForUserId;
    }
    // ========================================================================
    // ACCESS CHECKING METHODS
    // ========================================================================
    /**
     * Check the user's access status for an application.
     * First checks role-based authorization (ApplicationRole records), then installation status.
     * @param applicationId - The application ID to check
     * @returns The user's access status for this application
     */
    CheckUserApplicationAccess(applicationId) {
        // Step 1: Check role-based authorization first
        if (!this.UserHasApplicationAccess(applicationId)) {
            return 'not_authorized';
        }
        // Step 2: Then check installation status (existing logic)
        const userApp = this.GetUserApplicationByAppId(applicationId);
        if (!userApp) {
            return 'not_installed';
        }
        return userApp.IsActive ? 'installed_active' : 'installed_inactive';
    }
    /**
     * Read-only view of the ApplicationRole catalog. Used by permission providers
     * that need to reason about grants for arbitrary users (not just CurrentUser).
     */
    get ApplicationRoles() {
        return this._applicationRoles;
    }
    /**
     * Register a custom filter for application access checks. The filter is
     * called by `UserHasApplicationAccess()` after the default global role check.
     * It receives the default result and can override it based on custom logic
     * (e.g., tenant-scoped roles, feature flags, license restrictions).
     *
     * Only one filter can be registered at a time. Calling this again replaces
     * the previous filter. Pass `null` to remove the filter.
     *
     * @example
     * ```typescript
     * // Multi-tenant: scope access to the current tenant's role instead of the global union
     * UserInfoEngine.Instance.RegisterApplicationAccessFilter((appId, defaultAccess, user) => {
     *   const tenantRoleID = user.TenantContext?.roleID;
     *   if (!tenantRoleID) return defaultAccess; // no tenant context — fall back to default
     *   const appRoles = UserInfoEngine.Instance.ApplicationRoles
     *     .filter(ar => UUIDsEqual(ar.ApplicationID, appId));
     *   if (appRoles.length === 0) return true; // open access
     *   return appRoles.some(ar => UUIDsEqual(ar.RoleID, tenantRoleID) && ar.CanAccess);
     * });
     * ```
     */
    RegisterApplicationAccessFilter(filter) {
        this._applicationAccessFilter = filter;
    }
    /**
     * Register a custom filter for application admin checks. The filter is
     * called by `UserCanAdminApplication()` after the default global role check.
     * It receives the default result and can override it based on custom logic.
     *
     * Only one filter can be registered at a time. Calling this again replaces
     * the previous filter. Pass `null` to remove the filter.
     */
    RegisterApplicationAdminFilter(filter) {
        this._applicationAdminFilter = filter;
    }
    /**
     * Checks if the current user's roles grant access to the application.
     * If no ApplicationRole records exist for the app, access is open (backwards compatible).
     * If records exist, user must have at least one role with CanAccess=1.
     */
    UserHasApplicationAccess(applicationId) {
        const appRoles = this._applicationRoles.filter(ar => UUIDsEqual(ar.ApplicationID, applicationId));
        const md = this.ProviderToUse;
        const user = md.CurrentUser;
        // No role records = open access (backwards compatible)
        if (appRoles.length === 0) {
            // Still call filter — it may want to restrict open-access apps
            if (this._applicationAccessFilter && user) {
                return this._applicationAccessFilter(applicationId, true, user);
            }
            return true;
        }
        // Check if any of the user's roles have CanAccess=1
        if (!user || !user.UserRoles)
            return false;
        const defaultAccess = user.UserRoles.some(ur => appRoles.some(ar => UUIDsEqual(ar.RoleID, ur.RoleID) && ar.CanAccess));
        // Call through registered filter if present
        if (this._applicationAccessFilter) {
            return this._applicationAccessFilter(applicationId, defaultAccess, user);
        }
        return defaultAccess;
    }
    /**
     * Checks if the current user can administer a specific application.
     * Requires an explicit ApplicationRole record with CanAdmin=1.
     */
    UserCanAdminApplication(applicationId) {
        const appRoles = this._applicationRoles.filter(ar => UUIDsEqual(ar.ApplicationID, applicationId));
        const md = this.ProviderToUse;
        const user = md.CurrentUser;
        if (appRoles.length === 0) {
            // No admin without explicit grant — but still let filter override
            if (this._applicationAdminFilter && user) {
                return this._applicationAdminFilter(applicationId, false, user);
            }
            return false;
        }
        if (!user || !user.UserRoles)
            return false;
        const defaultCanAdmin = user.UserRoles.some(ur => appRoles.some(ar => UUIDsEqual(ar.RoleID, ur.RoleID) && ar.CanAdmin));
        // Call through registered filter if present
        if (this._applicationAdminFilter) {
            return this._applicationAdminFilter(applicationId, defaultCanAdmin, user);
        }
        return defaultCanAdmin;
    }
    /**
     * Check if an application is inactive in the system metadata.
     * @param applicationId - The application ID to check
     */
    IsApplicationInactive(applicationId) {
        const md = this.ProviderToUse;
        const appInfo = md.Applications.find((a) => UUIDsEqual(a.ID, applicationId));
        return appInfo != null && appInfo.Status !== 'Active';
    }
    /**
     * Get application info by ID from metadata.
     * @param applicationId - The application ID to find
     */
    GetApplicationInfo(applicationId) {
        const md = this.ProviderToUse;
        return md.Applications.find((a) => UUIDsEqual(a.ID, applicationId));
    }
    /**
     * Find application info by path or name (case-insensitive).
     * @param pathOrName - The path or name to search for
     */
    FindApplicationByPathOrName(pathOrName) {
        const normalized = pathOrName.trim().toLowerCase();
        const md = this.ProviderToUse;
        // First try path match
        const pathMatch = md.Applications.find((a) => a.Path?.toLowerCase() === normalized);
        if (pathMatch) {
            return pathMatch;
        }
        // Fallback to name match
        return md.Applications.find((a) => a.Name.trim().toLowerCase() === normalized);
    }
    /**
     * Force refresh all user data
     * @param contextUser - Optional user context for server-side use
     */
    async Refresh(contextUser) {
        await this.Config(true, contextUser);
    }
    // ========================================================================
    // APPLICATION MANAGEMENT METHODS
    // ========================================================================
    /**
     * Get only the active user applications (IsActive = true)
     */
    get ActiveUserApplications() {
        return this.UserApplications.filter((ua) => ua.IsActive);
    }
    /**
     * Check if user has a specific application installed (has UserApplication record)
     * @param applicationId - The application ID to check
     */
    HasApplication(applicationId) {
        return this.UserApplications.some((ua) => UUIDsEqual(ua.ApplicationID, applicationId));
    }
    /**
     * Check if user has a specific application active (installed AND IsActive = true)
     * @param applicationId - The application ID to check
     */
    HasActiveApplication(applicationId) {
        const userApp = this.GetUserApplicationByAppId(applicationId);
        return userApp != null && userApp.IsActive;
    }
    /**
     * Install an application for the current user by creating a UserApplication record.
     * The new record is automatically added to the cached UserApplications array.
     * @param applicationId - The application ID to install
     * @param contextUser - Optional user context for server-side use
     * @returns The newly created MJUserApplicationEntity, or null if failed
     */
    async InstallApplication(applicationId, contextUser) {
        const md = this.ProviderToUse;
        const userId = contextUser?.ID || md.CurrentUser?.ID;
        if (!userId) {
            console.error('UserInfoEngine.InstallApplication: No user context available');
            return null;
        }
        // Check if already installed
        const existingApp = this.GetUserApplicationByAppId(applicationId);
        if (existingApp) {
            // If already active, just return it
            if (existingApp.IsActive) {
                return existingApp;
            }
            // If disabled, enable it
            try {
                existingApp.IsActive = true;
                const saved = await existingApp.Save();
                if (saved) {
                    console.log(`UserInfoEngine.InstallApplication: Enabled existing application ${applicationId}`);
                    return existingApp;
                }
                else {
                    console.error('UserInfoEngine.InstallApplication: Failed to enable:', existingApp.LatestResult);
                    return null;
                }
            }
            catch (error) {
                console.error('UserInfoEngine.InstallApplication: Error enabling:', error instanceof Error ? error.message : String(error));
                return null;
            }
        }
        // Get the next sequence number
        const nextSequence = this.UserApplications.length;
        try {
            const userApp = await md.GetEntityObject('MJ: User Applications', contextUser);
            userApp.NewRecord();
            userApp.UserID = userId;
            userApp.ApplicationID = applicationId;
            userApp.Sequence = nextSequence;
            userApp.IsActive = true;
            const saved = await userApp.Save();
            if (saved) {
                // Add to cached array
                this._UserApplications.push(userApp);
                console.log(`UserInfoEngine.InstallApplication: Installed application ${applicationId} for user ${userId}`);
                return userApp;
            }
            else {
                console.error('UserInfoEngine.InstallApplication: Failed to save:', userApp.LatestResult);
                return null;
            }
        }
        catch (error) {
            console.error('UserInfoEngine.InstallApplication: Error:', error instanceof Error ? error.message : String(error));
            return null;
        }
    }
    /**
     * Enable a disabled application for the current user (set IsActive = true).
     * @param applicationId - The application ID to enable
     * @param contextUser - Optional user context for server-side use
     * @returns true if successful, false otherwise
     */
    async EnableApplication(applicationId, contextUser) {
        const userApp = this.GetUserApplicationByAppId(applicationId);
        if (!userApp) {
            console.warn(`UserInfoEngine.EnableApplication: No UserApplication record found for ${applicationId}`);
            return false;
        }
        if (userApp.IsActive) {
            console.log(`UserInfoEngine.EnableApplication: Application ${applicationId} is already active`);
            return true;
        }
        try {
            userApp.IsActive = true;
            const saved = await userApp.Save();
            if (saved) {
                console.log(`UserInfoEngine.EnableApplication: Enabled application ${applicationId}`);
                return true;
            }
            else {
                console.error('UserInfoEngine.EnableApplication: Failed to save:', userApp.LatestResult);
                return false;
            }
        }
        catch (error) {
            console.error('UserInfoEngine.EnableApplication: Error:', error instanceof Error ? error.message : String(error));
            return false;
        }
    }
    /**
     * Disable an application for the current user (set IsActive = false).
     * This does not delete the UserApplication record, just deactivates it.
     * @param applicationId - The application ID to disable
     * @param contextUser - Optional user context for server-side use
     * @returns true if successful, false otherwise
     */
    async DisableApplication(applicationId, contextUser) {
        const userApp = this.GetUserApplicationByAppId(applicationId);
        if (!userApp) {
            console.warn(`UserInfoEngine.DisableApplication: No UserApplication record found for ${applicationId}`);
            return false;
        }
        if (!userApp.IsActive) {
            console.log(`UserInfoEngine.DisableApplication: Application ${applicationId} is already disabled`);
            return true;
        }
        try {
            userApp.IsActive = false;
            const saved = await userApp.Save();
            if (saved) {
                console.log(`UserInfoEngine.DisableApplication: Disabled application ${applicationId}`);
                return true;
            }
            else {
                console.error('UserInfoEngine.DisableApplication: Failed to save:', userApp.LatestResult);
                return false;
            }
        }
        catch (error) {
            console.error('UserInfoEngine.DisableApplication: Error:', error instanceof Error ? error.message : String(error));
            return false;
        }
    }
    /**
     * Uninstall an application by deleting the UserApplication record.
     * @param applicationId - The application ID to uninstall
     * @param contextUser - Optional user context for server-side use
     * @returns true if successful, false otherwise
     */
    async UninstallApplication(applicationId, contextUser) {
        const userApp = this.GetUserApplicationByAppId(applicationId);
        if (!userApp) {
            console.warn(`UserInfoEngine.UninstallApplication: No UserApplication record found for ${applicationId}`);
            return false;
        }
        try {
            const deleted = await userApp.Delete();
            if (deleted) {
                // Remove from cached array
                const index = this._UserApplications.findIndex((ua) => UUIDsEqual(ua.ApplicationID, applicationId));
                if (index >= 0) {
                    this._UserApplications.splice(index, 1);
                }
                console.log(`UserInfoEngine.UninstallApplication: Uninstalled application ${applicationId}`);
                return true;
            }
            else {
                console.error('UserInfoEngine.UninstallApplication: Failed to delete:', userApp.LatestResult);
                return false;
            }
        }
        catch (error) {
            console.error('UserInfoEngine.UninstallApplication: Error:', error instanceof Error ? error.message : String(error));
            return false;
        }
    }
    /**
     * Create default UserApplication records for apps marked with DefaultForNewUser=true.
     * This is called automatically when a user has no UserApplication records.
     *
     * This method is safe to call multiple times concurrently - it will return the same
     * promise if already in progress, and will skip apps that already have UserApplication records.
     *
     * @param contextUser - Optional user context for server-side use
     * @returns Array of created MJUserApplicationEntity records
     */
    async CreateDefaultApplications(contextUser) {
        // If already in progress, return the existing promise to prevent duplicate execution
        if (this._createDefaultAppsPromise) {
            console.log('UserInfoEngine.CreateDefaultApplications: Already in progress, returning existing promise');
            return this._createDefaultAppsPromise;
        }
        // Create and store the promise, then clear it when done
        this._createDefaultAppsPromise = this.doCreateDefaultApplications(contextUser);
        try {
            return await this._createDefaultAppsPromise;
        }
        finally {
            // Clear the promise when done (success or failure)
            this._createDefaultAppsPromise = null;
        }
    }
    /**
     * Checks the database directly for this user's UserApplication records.
     * If the DB has records but our in-memory cache is empty (load failure),
     * repairs the cache and emits a property change so subscribers update.
     * @returns The DB records if repair occurred, empty array otherwise
     */
    async repairUserApplicationsFromDatabase(userId, contextUser) {
        try {
            const rv = new RunView(this.RunViewProviderToUse);
            const dbResult = await rv.RunView({
                EntityName: 'MJ: User Applications',
                ExtraFilter: `UserID='${userId}'`,
                ResultType: 'entity_object',
                BypassCache: true
            }, contextUser ?? this.ContextUser);
            if (dbResult.Success && dbResult.Results.length > 0) {
                LogStatus(`UserInfoEngine: Repaired _UserApplications from database (${dbResult.Results.length} records) — in-memory cache was empty`);
                this._UserApplications = dbResult.Results;
                this.emitPropertyChange('_UserApplications');
                return dbResult.Results;
            }
        }
        catch (error) {
            console.error('UserInfoEngine.repairUserApplicationsFromDatabase: DB verification failed:', error instanceof Error ? error.message : String(error));
        }
        return [];
    }
    /**
     * Internal implementation of CreateDefaultApplications.
     * Separated to allow the public method to manage the promise state.
     */
    async doCreateDefaultApplications(contextUser) {
        const md = this.ProviderToUse;
        const userId = contextUser?.ID || md.CurrentUser?.ID;
        if (!userId) {
            console.error('UserInfoEngine.CreateDefaultApplications: No user context available');
            return [];
        }
        // Verify against the database before creating — if the in-memory cache is empty
        // due to a load failure (e.g., cache timestamp bug), we'd otherwise attempt to
        // create records that already exist, hitting unique constraint violations.
        const userAppsForUser = this._UserApplications.filter((ua) => UUIDsEqual(ua.UserID, userId));
        if (userAppsForUser.length === 0) {
            const repaired = await this.repairUserApplicationsFromDatabase(userId, contextUser);
            if (repaired.length > 0) {
                return repaired;
            }
        }
        // Get existing UserApplication records for this user to prevent duplicates
        const existingAppIds = new Set(this._UserApplications.filter((ua) => UUIDsEqual(ua.UserID, userId)).map((ua) => ua.ApplicationID));
        // Filter to Active apps with DefaultForNewUser=true, sorted by DefaultSequence
        // Exclude apps that already have UserApplication records
        const defaultApps = md.Applications.filter((a) => a.DefaultForNewUser && a.Status === 'Active' && !existingAppIds.has(a.ID)).sort((a, b) => (a.DefaultSequence ?? 100) - (b.DefaultSequence ?? 100));
        if (defaultApps.length === 0) {
            console.log('UserInfoEngine.CreateDefaultApplications: No new apps to install (all defaults already exist)');
            return [];
        }
        console.log(`UserInfoEngine.CreateDefaultApplications: Found ${defaultApps.length} default apps to install`);
        const createdUserApps = [];
        // Calculate starting sequence based on existing apps
        const maxExistingSequence = this._UserApplications.filter((ua) => UUIDsEqual(ua.UserID, userId)).reduce((max, ua) => Math.max(max, ua.Sequence), -1);
        for (const [index, appInfo] of defaultApps.entries()) {
            try {
                const userApp = await md.GetEntityObject('MJ: User Applications', contextUser);
                userApp.NewRecord();
                userApp.UserID = userId;
                userApp.ApplicationID = appInfo.ID;
                userApp.Sequence = maxExistingSequence + 1 + index;
                userApp.IsActive = true;
                const saved = await userApp.Save();
                if (saved) {
                    this._UserApplications.push(userApp);
                    createdUserApps.push(userApp);
                    console.log(`UserInfoEngine.CreateDefaultApplications: Created UserApplication for ${appInfo.Name}`);
                }
                else {
                    console.error(`UserInfoEngine.CreateDefaultApplications: Failed to create for ${appInfo.Name}:`, userApp.LatestResult);
                }
            }
            catch (error) {
                console.error(`UserInfoEngine.CreateDefaultApplications: Error for ${appInfo.Name}:`, error instanceof Error ? error.message : String(error));
            }
        }
        return createdUserApps;
    }
    /**
     * Get all notification preferences for the current user
     */
    get NotificationPreferences() {
        if (!this._loadedForUserId)
            return [];
        return this.GetConfigData('_UserNotificationPreferences')
            .filter((p) => UUIDsEqual(p.UserID, this._loadedForUserId));
    }
    GetUserPreferenceForType(userId, typeId) {
        return (this._UserNotificationPreferences || []).find((p) => UUIDsEqual(p.UserID, userId) && UUIDsEqual(p.NotificationTypeID, typeId));
    }
    /**
     * Get current user's preference for a specific notification type
     */
    GetCurrentUserPreferenceForType(typeId) {
        if (!this._loadedForUserId)
            return undefined;
        return this.GetUserPreferenceForType(this._loadedForUserId, typeId);
    }
    /**
     * Get all notification types.
     * Notification types are global (not user-specific) and define the available notification categories.
     */
    get NotificationTypes() {
        return this.GetConfigData('_NotificationTypes');
    }
};
UserInfoEngine = __decorate([
    RegisterForStartup()
], UserInfoEngine);
export { UserInfoEngine };
//# sourceMappingURL=UserInfoEngine.js.map