import { configInfo } from '../config.js';
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
import { Metadata, UserInfo } from '@memberjunction/core';
import { NewUserBase } from './newUsers.js';
import { MJGlobal } from '@memberjunction/global';
import { AuthProviderFactory } from './AuthProviderFactory.js';
import { initializeAuthProviders } from './initializeProviders.js';
export { TokenExpiredError } from './tokenExpiredError.js';
export { AuthProviderFactory } from './AuthProviderFactory.js';
export * from './APIKeyScopeAuth.js';
// This is a hard-coded forever constant due to internal migrations
class MissingAuthError extends Error {
    constructor() {
        super('No authentication providers configured. Please configure at least one auth provider in mj.config.cjs');
        this.name = 'MissingAuthError';
    }
}
const refreshUserCache = async (dataSource) => {
    const startTime = Date.now();
    await UserCache.Instance.Refresh(dataSource);
    const endTime = Date.now();
    const elapsed = endTime - startTime;
    // if elapsed time is less than the delay setting, wait for the additional time to achieve the full delay
    // the below also makes sure we never go more than a 30 second total delay
    const delay = configInfo.userHandling.updateCacheWhenNotFoundDelay
        ? configInfo.userHandling.updateCacheWhenNotFoundDelay < 30000
            ? configInfo.userHandling.updateCacheWhenNotFoundDelay
            : 30000
        : 0;
    if (elapsed < delay)
        await new Promise((resolve) => setTimeout(resolve, delay - elapsed));
    const finalTime = Date.now();
    const finalElapsed = finalTime - startTime;
    console.log(`   UserCache updated in ${elapsed}ms, total elapsed time of ${finalElapsed}ms including delay of ${delay}ms (if needed). Attempting to find the user again via recursive call`);
};
/**
 * Gets validation options for a specific issuer.
 * When multiple providers share the same issuer (e.g. two Auth0 apps on
 * the same domain with different audiences/client IDs), all unique audiences
 * are aggregated into an array. jwt.verify() natively accepts string | string[].
 */
export const getValidationOptions = (issuer) => {
    const factory = AuthProviderFactory.getInstance();
    const providers = factory.getAllByIssuer(issuer);
    if (providers.length === 0) {
        return undefined;
    }
    // Collect unique audiences from all providers matching this issuer
    const audiences = [...new Set(providers.map(p => p.audience))];
    return {
        audience: audiences.length === 1 ? audiences[0] : audiences,
        jwksUri: providers[0].jwksUri // Same issuer = same JWKS endpoint
    };
};
/**
 * Backward compatible validationOptions object
 * @deprecated Use getValidationOptions() or AuthProviderRegistry instead
 */
export const validationOptions = new Proxy({}, {
    get: (target, prop) => {
        return getValidationOptions(prop);
    },
    has: (target, prop) => {
        return getValidationOptions(prop) !== undefined;
    },
    ownKeys: () => {
        const factory = AuthProviderFactory.getInstance();
        return factory.getAllProviders().map(p => p.issuer);
    }
});
export class UserPayload {
}
/**
 * Gets signing keys for JWT validation
 */
export const getSigningKeys = (issuer) => (header, cb) => {
    const factory = AuthProviderFactory.getInstance();
    // Initialize providers if not already done
    if (!factory.hasProviders()) {
        initializeAuthProviders();
    }
    const provider = factory.getByIssuer(issuer);
    if (!provider) {
        // Check if we have any providers at all
        if (!factory.hasProviders()) {
            throw new MissingAuthError();
        }
        throw new Error(`No authentication provider found for issuer: ${issuer}`);
    }
    provider.getSigningKey(header, cb);
};
/**
 * Extracts user information from JWT payload using the appropriate provider
 */
export const extractUserInfoFromPayload = (payload) => {
    const factory = AuthProviderFactory.getInstance();
    const issuer = payload.iss;
    if (!issuer) {
        // Fallback to default extraction
        const preferredUsername = payload.preferred_username;
        return {
            email: payload.email || preferredUsername,
            firstName: payload.given_name,
            lastName: payload.family_name,
            fullName: payload.name,
            preferredUsername
        };
    }
    const provider = factory.getByIssuer(issuer);
    if (!provider) {
        // Fallback to default extraction
        const fullName = payload.name;
        const preferredUsername = payload.preferred_username;
        return {
            email: payload.email || preferredUsername,
            firstName: payload.given_name || fullName?.split(' ')[0],
            lastName: payload.family_name || fullName?.split(' ')[1] || fullName?.split(' ')[0],
            fullName,
            preferredUsername
        };
    }
    return provider.extractUserInfo(payload);
};
export const getSystemUser = async (dataSource, attemptCacheUpdateIfNeeded = true) => {
    const systemUser = UserCache.Instance.GetSystemUser();
    if (!systemUser) {
        if (dataSource && attemptCacheUpdateIfNeeded) {
            console.warn(`System user not found in cache. Updating cache in attempt to find the user...`);
            await refreshUserCache(dataSource);
            return getSystemUser(dataSource, false); // try one more time but do not update cache next time if not found
        }
        throw new Error(`System user ID '${UserCache.Instance.SYSTEM_USER_ID}' not found in database`);
    }
    return systemUser;
};
export const verifyUserRecord = async (email, firstName, lastName, requestDomain, dataSource, attemptCacheUpdateIfNeeded = true) => {
    if (!email)
        return undefined;
    let user = UserCache.Instance.Users.find((u) => {
        if (!u.Email || u.Email.trim() === '') {
            // this condition should never occur. If it doesn throw a console error including the user id
            // DB requires non-null but this is just an extra check and we could in theory have a blank string in the DB
            console.error(`SYSTEM METADATA ISSUE: User ${u.ID} has no email address`);
            return false;
        }
        else
            return u.Email.toLowerCase().trim() === email.toLowerCase().trim();
    });
    if (!user) {
        if (configInfo.userHandling.autoCreateNewUsers &&
            firstName &&
            lastName &&
            (requestDomain || configInfo.userHandling.newUserLimitedToAuthorizedDomains === false)) {
            // check to see if the domain that we have a request coming in from matches one of the domains in the autoCreateNewUsersDomains setting
            let passesDomainCheck = configInfo.userHandling.newUserLimitedToAuthorizedDomains ===
                false; /*in this first condition, we are set up to NOT care about domain */
            if (!passesDomainCheck && requestDomain) {
                /*in this second condition, we check the domain against authorized domains*/
                passesDomainCheck = configInfo.userHandling.newUserAuthorizedDomains.some((pattern) => {
                    // Convert wildcard domain patterns to regular expressions
                    const regex = new RegExp('^' + pattern.toLowerCase().trim().replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
                    return regex.test(requestDomain?.toLowerCase().trim());
                });
            }
            if (passesDomainCheck) {
                // we have a domain from the request that matches one of the domains provided by the configuration, so we will create a new user
                console.warn(`User ${email} not found in cache. Attempting to create a new user...`);
                const newUserCreator = MJGlobal.Instance.ClassFactory.CreateInstance(NewUserBase); // this will create the object that handles creating the new user for us
                const newUser = await newUserCreator.createNewUser(firstName, lastName, email);
                if (newUser) {
                    // new user worked! we already have the stuff we need for the cache, so no need to go to the DB now, just create a new UserInfo object and use the return value from the createNewUser method
                    // to init it, including passing in the role list for the user.
                    const md = new Metadata();
                    const initData = newUser.GetAll();
                    initData.UserRoles = configInfo.userHandling.newUserRoles.map((role) => {
                        const roleInfo = md.Roles.find((r) => r.Name === role);
                        const roleID = roleInfo ? roleInfo.ID : '';
                        return { UserID: initData.ID, RoleName: role, RoleID: roleID };
                    });
                    user = new UserInfo(Metadata.Provider, initData);
                    UserCache.Instance.Users.push(user);
                    console.warn(`   >>> New user ${email} created successfully!`);
                }
            }
            else {
                console.warn(`User ${email} not found in cache. Request domain '${requestDomain}' does not match any of the domains in the newUserAuthorizedDomains setting. To ignore domain, make sure you set the newUserLimitedToAuthorizedDomains setting to false. In this case we are NOT creating a new user.`);
            }
        }
        if (!user && configInfo.userHandling.updateCacheWhenNotFound && dataSource && attemptCacheUpdateIfNeeded) {
            // if we get here that means in the above, if we were attempting to create a new user, it did not work, or it wasn't attempted and we have a config that asks us to auto update the cache
            console.warn(`User ${email} not found in cache. Updating cache in attempt to find the user...`);
            await refreshUserCache(dataSource);
            return verifyUserRecord(email, firstName, lastName, requestDomain, dataSource, false); // try one more time but do not update cache next time if not found
        }
    }
    return user;
};
// Initialize providers on module load
initializeAuthProviders();
//# sourceMappingURL=index.js.map