"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ALPHA_ORGS = exports.ENTERPRISE_SITES_SERVICE_STATUS = exports.HUB_SERVICE_STATUS = exports.ArcGISContextManager = void 0;
const arcgis_rest_portal_1 = require("@esri/arcgis-rest-portal");
const arcgis_rest_request_1 = require("@esri/arcgis-rest-request");
const ArcGISContext_1 = require("./ArcGISContext");
const getHubApiFromPortalUrl_1 = require("./urls/getHubApiFromPortalUrl");
const getPortalBaseFromOrgUrl_1 = require("./urls/getPortalBaseFromOrgUrl");
const logger_1 = require("./utils/logger");
const util_1 = require("./util");
const encoding_1 = require("./utils/encoding");
const arcgis_rest_request_2 = require("@esri/arcgis-rest-request");
const fail_safe_1 = require("./utils/fail-safe");
const hubUserAppResources_1 = require("./utils/hubUserAppResources");
const fetchAndMigrateUserHubSettings_1 = require("./utils/internal/fetchAndMigrateUserHubSettings");
const fetchOrgLimits_1 = require("./org/fetchOrgLimits");
const get_prop_1 = require("./objects/get-prop");
const get_with_default_1 = require("./objects/get-with-default");
const isEnterprisePortalUrl_1 = require("./urls/isEnterprisePortalUrl");
/**
 * Properties that we can always serialize/deserialize regardless of authentication status
 */
const CONTEXT_SERIALIZABLE_PROPS = [
    "portalUrl",
    "serviceStatus",
    "featureFlags",
    "properties",
    "resourceConfigs",
    "trustedOrgIds",
    "trustedOrgs",
];
/**
 * Properties that we only serialize/deserialize if the user is authenticated
 */
const CONTEXT_AUTHD_SERIALIZABLE_PROPS = ["resourceTokens", "portal", "currentUser", "portalSettings"];
/**
 * The manager exposes context (`IArcGISContext`), which combines a `ArcGISIdentityManager` with
 * the `portal/self` and `user/self` responses to provide a central lookup for platform
 * information, api urls, and other useful properties for developers such as IRequestOptions
 * IUserRequestOptions, IHubRequestOptions etc.
 *
 * The context is exposed on gthe `.context` property, and as the authentication changes
 * the `.context` is re-created. This is done to allow web frameworks to watch for
 * changes on that single property, instead of having to leverage observers or events
 * for change detection.
 *
 * Please see the [ArcGISContext Guide](/hub.js/guides/context) for additional information.
 *
 */
class ArcGISContextManager {
    /**
     * Private constructor. Use `ArcGISContextManager.create(...)` to
     * instantiate an instance
     * @param opts
     */
    constructor(opts) {
        this._portalUrl = "https://www.arcgis.com";
        this._properties = {};
        this._featureFlags = {};
        this._trustedOrgIds = [];
        this._trustedOrgs = [];
        this._resourceConfigs = [];
        this._userResourceTokens = [];
        // Having a unique id makes debugging easier
        this.id = new Date().getTime();
        logger_1.Logger.debug(`ArcGISContextManager:ctor: Creating ${this.id}`);
        if (opts.properties) {
            this._properties = opts.properties;
        }
        // Default to the Alpha orgs defined in Hub.js unless
        // other values are passed in
        if (!this._properties.alphaOrgs) {
            this._properties.alphaOrgs = [...exports.ALPHA_ORGS];
        }
        if (opts.authentication) {
            this._authentication = opts.authentication;
            this._portalUrl = this._authentication.portal.replace("/sharing/rest", "");
            this._hubUrl = (0, getHubApiFromPortalUrl_1.getHubApiFromPortalUrl)(this._portalUrl);
        }
        else if (opts.portalUrl) {
            this._portalUrl = opts.portalUrl;
            this._hubUrl = (0, getHubApiFromPortalUrl_1.getHubApiFromPortalUrl)(this._portalUrl);
        }
        else {
            this._hubUrl = (0, getHubApiFromPortalUrl_1.getHubApiFromPortalUrl)(this._portalUrl);
        }
        if (opts.portal) {
            this._portal = (0, util_1.cloneObject)(opts.portal);
        }
        if (opts.portalSettings) {
            this._portalSettings = (0, util_1.cloneObject)(opts.portalSettings);
        }
        if (opts.currentUser) {
            this._currentUser = (0, util_1.cloneObject)(opts.currentUser);
        }
        if (opts.serviceStatus) {
            this._serviceStatus = opts.serviceStatus;
        }
        if (opts.featureFlags) {
            this._featureFlags = (0, util_1.cloneObject)(opts.featureFlags);
        }
        if (opts.serviceFlags) {
            this._serviceFlags = (0, util_1.cloneObject)(opts.serviceFlags);
        }
        if (opts.trustedOrgIds) {
            this._trustedOrgIds = (0, util_1.cloneObject)(opts.trustedOrgIds);
        }
        if (opts.trustedOrgs) {
            this._trustedOrgs = (0, util_1.cloneObject)(opts.trustedOrgs);
        }
        if (opts.resourceConfigs) {
            this._resourceConfigs = (0, util_1.cloneObject)(opts.resourceConfigs);
        }
        if (opts.resourceTokens) {
            this._userResourceTokens = (0, util_1.cloneObject)(opts.resourceTokens);
        }
        if (opts.userHubSettings) {
            this._userHubSettings = (0, util_1.cloneObject)(opts.userHubSettings);
        }
    }
    /**
     * Used to create a new instance of the ArcGISContext class.
     *
     * ```js
     * const ctxMgr = await ArcGISContextManager.create();
     * ```
     *
     * @param opts
     * @returns
     */
    static async create(opts = {}) {
        const ctx = new ArcGISContextManager(opts);
        await ctx.initialize();
        return ctx;
    }
    /**
     * Create a new instance of the ArcGISContextManager from a serialized
     * string. This is useful when you want to store the context in a
     * browser's local storage or server side session.
     * @param serializedContext
     * @returns
     */
    static async deserialize(serializedContext) {
        // decode the string
        const decoded = (0, encoding_1.base64ToUnicode)(serializedContext);
        const state = JSON.parse(decoded);
        // create opts and populate from state
        let opts = {};
        //
        // iterate the serialized props
        CONTEXT_SERIALIZABLE_PROPS.forEach((prop) => {
            opts = (0, util_1.maybeAdd)(prop, (0, get_prop_1.getProp)(state, prop), opts);
        });
        // check if there is a session and if it's still valid
        if (state.session) {
            const userSession = arcgis_rest_request_1.ArcGISIdentityManager.deserialize(state.session);
            if (userSession.tokenExpires.getTime() > Date.now()) {
                CONTEXT_AUTHD_SERIALIZABLE_PROPS.forEach((prop) => {
                    opts = (0, util_1.maybeAdd)(prop, (0, get_prop_1.getProp)(state, prop), opts);
                });
                // CRTITICAL: This must be done after the maybeAdd calls above
                // as those return clones of the opts object, and cloneObject
                // does not support cloning a ArcGISIdentityManager
                opts.authentication = userSession;
            }
        }
        return ArcGISContextManager.create(opts);
    }
    /**
     * Serialize the context into a string that can be stored
     * and re-hydrated
     * @returns
     */
    serialize() {
        let state = {};
        // iterate the serializable props...
        CONTEXT_SERIALIZABLE_PROPS.forEach((prop) => {
            state = (0, util_1.maybeAdd)(prop, (0, get_prop_1.getProp)(this, `_${prop}`), state);
        });
        // If user is authenticated, serialize the session and other auth related props
        if (this._authentication) {
            state.session = this._authentication.serialize();
            CONTEXT_AUTHD_SERIALIZABLE_PROPS.forEach((prop) => {
                state = (0, util_1.maybeAdd)(prop, (0, get_prop_1.getProp)(this, `_${prop}`), state);
            });
        }
        return (0, encoding_1.unicodeToBase64)(JSON.stringify(state));
    }
    /**
     * Set the Authentication (ArcGISIdentityManager) for the context.
     * This should be called when a user signs into a running
     * application.
     * @param auth
     */
    async setAuthentication(auth) {
        this._authentication = auth;
        this._portalUrl = auth.portal.replace("/sharing/rest", "");
        await this.initialize();
    }
    /**
     * Set the properties hash and re-create the context
     * @param properties
     */
    setProperties(properties) {
        this._properties = properties;
        this._context = new ArcGISContext_1.ArcGISContext(this.contextOpts);
    }
    /**
     * Clear the Authentication (ArcGISIdentityManager). This should be
     * called when a user signs out of an application, but
     * the application continues running
     */
    clearAuthentication() {
        // Reset the portalUrl from the org url to the base url
        // for ArcGIS Enterprise, we just leave the _portalUrl as-is
        if (!this._context.isPortal) {
            this._portalUrl = (0, getPortalBaseFromOrgUrl_1.getPortalBaseFromOrgUrl)(this._portalUrl);
        }
        // Clear the auth related props
        this._authentication = null;
        this._portal = null;
        this._currentUser = null;
        this._userResourceTokens = [];
        // re-create the context
        this._context = new ArcGISContext_1.ArcGISContext(this.contextOpts);
    }
    /**
     * Return a reference to the current state.
     * When `.setAuthentication()` or `.clearAuthenentication()` are
     * called, the state will be re-created. This is done so frameworks
     * like React or Ember can detect changes.
     */
    get context() {
        return this._context;
    }
    /**
     * Update the User's Hub Settings
     * Stores in the user-app-resource associated with the `hubforarcgis` clientId
     * and updates the context
     * @param settings
     */
    async updateUserHubSettings(settings) {
        if (!this._authentication) {
            throw new Error("Cannot update user hub settings without an authenticated user");
        }
        // update the user-app-resource
        await (0, hubUserAppResources_1.updateUserHubSettings)(settings, this.context, true);
        // update the context
        this._userHubSettings = settings;
        // update the feature flags
        Object.keys((0, get_with_default_1.getWithDefault)(settings, "features", {})).forEach((key) => {
            const value = (0, get_prop_1.getProp)(settings, `features.${key}`);
            this._featureFlags[`hub:feature:${key}`] = value;
        });
        this._context = new ArcGISContext_1.ArcGISContext(this.contextOpts);
    }
    /**
     * If we have a ArcGISIdentityManager, fetch other resources to populate the context
     */
    async initialize() {
        // setup array to hold promises and one to track promise index
        const promises = [];
        const promiseKeys = [];
        // We always want service status if it's not passed in
        if (!this._serviceStatus) {
            promises.push(getServiceStatus(this._portalUrl));
            promiseKeys.push("serviceStatus");
        }
        // Other info is only relevant if we have authentication
        // and we only want to fetch the info, if it was not passed in
        if (this._authentication) {
            const token = await this._authentication.getToken(this._authentication.portal);
            if (!this._portal) {
                promises.push(getSelfWithLimits(this._authentication));
                promiseKeys.push("portal");
            }
            if (!this._portalSettings) {
                const failSafeGetPortalSettings = (0, fail_safe_1.failSafe)(arcgis_rest_portal_1.getPortalSettings, {});
                promises.push(failSafeGetPortalSettings("self", {
                    authentication: this._authentication,
                }));
                promiseKeys.push("portalSettings");
            }
            if (this._portal && !this._portal.limits) {
                promises.push(getPortalLimits(this._portal.id, this._authentication));
                promiseKeys.push("portalLimits");
            }
            if (!this._currentUser) {
                const username = this._authentication.username;
                promises.push((0, arcgis_rest_portal_1.getUser)({ username, authentication: this._authentication }));
                promiseKeys.push("user");
            }
            if (!this._trustedOrgs.length) {
                promises.push(getTrustedOrgs(this._portalUrl, this._authentication));
                promiseKeys.push("trustedOrgs");
            }
            // if we don't have the tokens but we have resource configs, fetch them
            if (!this._userResourceTokens.length && this._resourceConfigs.length) {
                promises.push(getUserResourceTokens(this._resourceConfigs, token, this._portalUrl + "/sharing/rest"));
                promiseKeys.push("tokens");
            }
            // always add "self" to resourceTokens, associated with the app's clientId
            this._userResourceTokens.push({
                app: "self",
                token,
                clientId: this._authentication.clientId || "self",
            });
        }
        // Await promises
        let results;
        try {
            results = await Promise.all(promises);
        }
        catch (ex) {
            const msg = `ArcGISContextManager failed while initializing for "${this._authentication.username}" using ${this._authentication.portal}.`;
            logger_1.Logger.error(msg);
            // tslint:disable-next-line:no-console
            console.error(msg);
            throw ex;
        }
        // iterate the keys, assigning values into private fields
        promiseKeys.forEach((key, idx) => {
            const result = results[idx];
            switch (key) {
                case "portal":
                    this._portal = result;
                    break;
                case "portalSettings":
                    this._portalSettings = result;
                    break;
                case "portalLimits":
                    this._portal.limits = result;
                    break;
                case "user":
                    this._currentUser = result;
                    break;
                case "trustedOrgs":
                    this._trustedOrgs = result;
                    this._trustedOrgIds = getTrustedOrgIds(this._trustedOrgs);
                    break;
                case "tokens":
                    this._userResourceTokens = [
                        ...this._userResourceTokens,
                        ...result,
                    ];
                    break;
                case "serviceStatus":
                    // eslint-disable-next-line no-case-declarations
                    const fetchedStatus = result;
                    // apply any overrides from serviceFlags
                    // We need to do this here because the _serviceFlags prop
                    // is passed into the ArcGISContext and that's what is used
                    // to determine the service status.
                    if (this._serviceFlags) {
                        Object.keys(this._serviceFlags).forEach((key) => {
                            // This guard is present because non-typescript code could pass in
                            // a serviceFlags object that does not have keys of HubServiceStatus.
                            if (fetchedStatus[key]) {
                                fetchedStatus[key] = this
                                    ._serviceFlags[key];
                            }
                        });
                    }
                    this._serviceStatus = fetchedStatus;
                    break;
            }
        });
        // if we are auth'd and have a hubforarcgis token,
        // fetch the users IUserHubSettings extract out the
        // preview properties and cross-walk to permissions
        // ======================================
        // NOTE: We don't call `fetchUserHubSettings` here because the "context"
        // is not created yet, so we cannot pass it in to that function.
        // ======================================
        const hubAppToken = this._userResourceTokens.find((e) => e.app === "hubforarcgis");
        if (this._authentication && hubAppToken) {
            this._userHubSettings = await (0, fetchAndMigrateUserHubSettings_1.fetchAndMigrateUserHubSettings)(this._authentication.username, this._portalUrl, hubAppToken.token);
            // Check for feature settings and walk the into feature flags
            Object.keys((0, get_with_default_1.getWithDefault)(this._userHubSettings, "features", {})).forEach((key) => {
                const val = (0, get_prop_1.getProp)(this._userHubSettings, `features.${key}`);
                this._featureFlags[`hub:feature:${key}`] = val;
            });
        }
        logger_1.Logger.debug(`ArcGISContextManager-${this.id}: updating context`);
        // update the context
        this._context = new ArcGISContext_1.ArcGISContext(this.contextOpts);
    }
    /**
     * Getter to streamline the creation of updated Context instances
     */
    get contextOpts() {
        // Prop names for the IArcGISContextOptions interface do not match
        // up exactly with the prop names on the serialized context so we have to define
        // separate arrays here
        const anonProps = ["hubUrl", ...CONTEXT_SERIALIZABLE_PROPS];
        const authdProps = [
            "currentUser",
            "userResourceTokens",
            "userHubSettings",
            "portalSettings",
        ];
        let contextOpts = {
            id: this.id,
            portalUrl: this._portalUrl,
        };
        // iterate the anon props...
        anonProps.forEach((prop) => {
            contextOpts = (0, util_1.maybeAdd)(prop, (0, get_prop_1.getProp)(this, `_${prop}`), contextOpts);
        });
        // If user is authenticated, we can add other props
        if (this._authentication) {
            authdProps.forEach((prop) => {
                contextOpts = (0, util_1.maybeAdd)(prop, (0, get_prop_1.getProp)(this, `_${prop}`), contextOpts);
            });
            // CRTITICAL: .authentication must be attached after the maybeAdd calls above
            // as those return clones of the opts object, and cloneObject
            // does not support cloning a ArcGISIdentityManager
            contextOpts.authentication = this._authentication;
        }
        // Handle some special cases where the prop names don't map cleanly
        // and would require a breaking change to fix
        contextOpts.portalSelf = this._portal;
        return contextOpts;
    }
}
exports.ArcGISContextManager = ArcGISContextManager;
/**
 * @internal
 * Fetch the service status for the given portalUrl
 * @param portalUrl
 * @returns
 */
async function getServiceStatus(portalUrl) {
    let status = exports.HUB_SERVICE_STATUS;
    // When we move to fetching the system status from the API
    // we can use
    // const hubApiUrl = getHubApiFromPortalUrl(portalUrl);
    if ((0, isEnterprisePortalUrl_1.isEnterprisePortalUrl)(portalUrl)) {
        status = exports.ENTERPRISE_SITES_SERVICE_STATUS;
    }
    return Promise.resolve(status);
}
/**
 * @internal
 * Get trusted orgs w/ a failSafe to return an empty array
 */
async function getTrustedOrgs(_portalUrl, _authentication) {
    const failSafeTrustedOrgs = (0, fail_safe_1.failSafe)(arcgis_rest_request_2.request, { trustedOrgs: [] });
    const response = (await failSafeTrustedOrgs(`${_portalUrl}/sharing/rest/portals/self/trustedOrgs?f=json`, {
        params: {
            token: _authentication.token,
        },
    }));
    return response.trustedOrgs;
}
/**
 * @internal
 * Extract trustedOrg ids from trustedOrgs response
 */
function getTrustedOrgIds(trustedOrgs) {
    return trustedOrgs.map((org) => org.to.orgId);
}
/**
 * @internal
 * Get the portal and its limits.
 * Note: we are only fetching a set of limits,
 * but we can add additional limits as needed
 *
 * @param authentication
 * @returns {IPortal}
 */
async function getSelfWithLimits(authentication) {
    const portal = await (0, arcgis_rest_portal_1.getSelf)({ authentication });
    const limits = await getPortalLimits(portal.id, authentication);
    portal.limits = limits;
    return portal;
}
/**
 * @internal
 * Get a set of limits for the given org.
 * Note: we can add additional limits as needed
 *
 * @param orgId
 * @param authentication
 */
async function getPortalLimits(orgId, authentication) {
    // TODO: add additional limits as needed
    const limitsToFetch = [{ type: "Groups", name: "MaxNumUserGroups", fallback: 512 }];
    const limits = await Promise.all(limitsToFetch.map(async (limit) => {
        try {
            const resolvedLimit = await (0, fetchOrgLimits_1.fetchOrgLimits)(orgId, limit.type, limit.name, {
                authentication,
                portal: authentication.portal,
            });
            return resolvedLimit;
        }
        catch (_error) {
            return {
                type: limit.type,
                name: limit.name,
                limitValue: limit.fallback,
            };
        }
    }));
    return limits.reduce((acc, limit) => {
        acc[limit.name] = limit.limitValue;
        return acc;
    }, {});
}
/**
 * @internal
 * Get a set of tokens for the given configs
 * @param configs
 * @param currentToken
 * @param portalUrl
 * @returns
 */
async function getUserResourceTokens(configs, currentToken, portalUrl) {
    // failSafe exchangeToken so we don't have to catch
    const failSafeExchange = (0, fail_safe_1.failSafe)(arcgis_rest_request_1.exchangeToken, null);
    const promises = configs.map((cfg) => {
        return failSafeExchange(currentToken, cfg.clientId, portalUrl).then((token) => {
            if (token) {
                return Object.assign(Object.assign({}, cfg), { token });
            }
        });
    });
    const results = await Promise.all(promises);
    // remove any null entries so downstream code doesn't have to check
    return results.filter((e) => !!e);
}
exports.HUB_SERVICE_STATUS = {
    portal: "online",
    discussions: "online",
    events: "online",
    metrics: "online",
    notifications: "online",
    "hub-search": "online",
    domains: "online",
    "hub-downloads": "online",
    "hub-ai-assistant": "online",
};
exports.ENTERPRISE_SITES_SERVICE_STATUS = {
    portal: "online",
    discussions: "not-available",
    events: "not-available",
    metrics: "not-available",
    notifications: "not-available",
    "hub-search": "not-available",
    domains: "not-available",
    "hub-downloads": "not-available",
    "hub-ai-assistant": "not-available",
};
const DEV_ALPHA_ORGS = [
    "LjjARY1mkhxulWPq",
    "q2ikdtW0bkt5EgtQ",
    "yHYVvboBBOdmcKci",
];
const QA_ALPHA_ORGS = [
    "97KLIFOSt5CxbiRI",
    "MiFBHFxEZWumnKCx",
    "8HRYeOqprj872mxP",
    "Xj56SBi2udA78cC9",
    "uFEMdY4VMonzH8sG",
];
const PROD_ALPHA_ORGS = [
    // "gGHDlz6USftL5Pau",
    "CrA5hYOKgL3Vwan8",
    "zj227gjeSqEyG4HF",
    "bkrWlSKcjUDFDtgw",
    "vIu5NCxQFilhVymO",
];
exports.ALPHA_ORGS = [
    ...PROD_ALPHA_ORGS,
    ...QA_ALPHA_ORGS,
    ...DEV_ALPHA_ORGS,
];
//# sourceMappingURL=ArcGISContextManager.js.map