"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.channelToSearchResult = void 0;
exports.isDiscussable = isDiscussable;
exports.setDiscussableKeyword = setDiscussableKeyword;
exports.isPublicChannel = isPublicChannel;
exports.isOrgChannel = isOrgChannel;
exports.isPrivateChannel = isPrivateChannel;
exports.getChannelAccess = getChannelAccess;
exports.getChannelOrgIds = getChannelOrgIds;
exports.getChannelGroupIds = getChannelGroupIds;
exports.getChannelUsersQuery = getChannelUsersQuery;
exports.getPostCSVFileName = getPostCSVFileName;
const arcgis_rest_portal_1 = require("@esri/arcgis-rest-portal");
const constants_1 = require("./constants");
const util_1 = require("../util");
const aclCategory_1 = require("./api/enums/aclCategory");
const sharingAccess_1 = require("./api/enums/sharingAccess");
const aclSubCategory_1 = require("./api/enums/aclSubCategory");
/**
 * Utility to determine if a given IGroup, IItem, IHubContent, or IHubItemEntity
 * is discussable.
 * @param {IGroup|IItem|IHubContent|IHubItemEntity} subject
 * @return {boolean}
 */
function isDiscussable(subject) {
    let result = false;
    if (subject) {
        const typeKeywords = (subject.typeKeywords || []);
        result = !typeKeywords.includes(constants_1.CANNOT_DISCUSS);
    }
    return result;
}
/**
 * Adds or removes CANNOT_DISCUSS type keyword and returns the updated list
 * @param {IGroup|IHubContent|IHubItemEntity} subject
 * @param {boolean} discussable
 * @returns {string[]} updated list of type keywords
 */
function setDiscussableKeyword(typeKeywords, discussable) {
    const updatedTypeKeywords = (typeKeywords || []).filter((typeKeyword) => typeKeyword !== constants_1.CANNOT_DISCUSS);
    if (!discussable) {
        updatedTypeKeywords.push(constants_1.CANNOT_DISCUSS);
    }
    return updatedTypeKeywords;
}
/**
 * Determines if the given channel is considered to be a `public` channel, supporting both
 * legacy permissions and V2 ACL model.
 * @param channel An IChannel record
 * @returns true if the channel is considered `public`
 */
function isPublicChannel(channel) {
    return channel.channelAcl
        ? channel.channelAcl.some(({ category }) => [aclCategory_1.AclCategory.AUTHENTICATED_USER, aclCategory_1.AclCategory.ANONYMOUS_USER].includes(category))
        : channel.access === sharingAccess_1.SharingAccess.PUBLIC;
}
/**
 * Determines if the given channel is considered to be an `org` channel, supporting both
 * legacy permissions and V2 ACL model.
 * @param channel An IChannel record
 * @returns true if the channel is considered `org`
 */
function isOrgChannel(channel) {
    return channel.channelAcl
        ? !isPublicChannel(channel) &&
            channel.channelAcl.some(({ category, subCategory }) => category === aclCategory_1.AclCategory.ORG &&
                subCategory === aclSubCategory_1.AclSubCategory.MEMBER)
        : channel.access === sharingAccess_1.SharingAccess.ORG;
}
/**
 * Determines if the given channel is considered to be a `private` channel, supporting both
 * legacy permissions and V2 ACL model.
 * @param channel An IChannel record
 * @returns true if the channel is considered `private`
 */
function isPrivateChannel(channel) {
    return !isPublicChannel(channel) && !isOrgChannel(channel);
}
/**
 * Determines the given channel's access, supporting both legacy permissions and V2 ACL
 * model.
 * @param channel An IChannel record
 * @returns `public`, `org` or `private`
 */
function getChannelAccess(channel) {
    let access = sharingAccess_1.SharingAccess.PRIVATE;
    if (isPublicChannel(channel)) {
        access = sharingAccess_1.SharingAccess.PUBLIC;
    }
    else if (isOrgChannel(channel)) {
        access = sharingAccess_1.SharingAccess.ORG;
    }
    return access;
}
/**
 * Returns an array of org ids configured for the channel, supporting both legacy permissions
 * and V2 ACL model.
 * @param channel An IChannel record
 * @returns an array of org ids for the given channel
 */
function getChannelOrgIds(channel) {
    return channel.channelAcl
        ? channel.channelAcl.reduce((acc, permission) => permission.category === aclCategory_1.AclCategory.ORG &&
            [aclSubCategory_1.AclSubCategory.MEMBER, aclSubCategory_1.AclSubCategory.ADMIN].includes(permission.subCategory) &&
            !acc.includes(permission.key)
            ? [...acc, permission.key]
            : acc, [])
        : channel.orgs;
}
/**
 * Returns an array of group ids configured for the channel, supporting both legacy permissions
 * and V2 ACL model.
 * @param channel An IChannel record
 * @returns an array of group ids for the given channel
 */
function getChannelGroupIds(channel) {
    return channel.channelAcl
        ? channel.channelAcl.reduce((acc, permission) => permission.category === aclCategory_1.AclCategory.GROUP &&
            [aclSubCategory_1.AclSubCategory.MEMBER, aclSubCategory_1.AclSubCategory.ADMIN].includes(permission.subCategory) &&
            !acc.includes(permission.key)
            ? [...acc, permission.key]
            : acc, [])
        : channel.groups;
}
/**
 * A utility method used to build an IQuery to search for users that are permitted to be at-mentioned for the given channel.
 * @param terms An array of strings to search for. Each string is mapped to `username` and `fullname`, filters as an OR condition
 * @param channel An IChannel record
 * @param currentUsername The currently authenticated user's username
 * @param requestOptions An IRequestOptions object
 * @returns a promise that resolves an IQuery
 */
async function getChannelUsersQuery(terms, channel, currentUsername, requestOptions) {
    const getNonOwnerAclKeysByCategory = (category, channel) => channel.channelAcl.reduce((acc, channelAcl) => channelAcl.category === category &&
        !acc[channelAcl.subCategory].includes(channelAcl.key)
        ? Object.assign(Object.assign({}, acc), { [channelAcl.subCategory]: [
                ...acc[channelAcl.subCategory],
                channelAcl.key,
            ] }) : acc, { member: [], admin: [] });
    const filters = [];
    if (isPublicChannel(channel)) {
        // any authenticated user when channel access is public
        filters.push({
            operation: "AND",
            predicates: [{ orgid: { from: "0", to: "{" } }],
        });
    }
    else {
        // either org or private
        const filter = {
            operation: "OR",
            predicates: [],
        };
        // process orgs
        const orgIdsBySubCategory = getNonOwnerAclKeysByCategory(aclCategory_1.AclCategory.ORG, channel);
        // org members
        if (orgIdsBySubCategory.member.length) {
            filter.predicates.push({
                orgid: orgIdsBySubCategory.member,
            });
        }
        // org admins
        if (orgIdsBySubCategory.admin.length) {
            filter.predicates.push({
                orgid: orgIdsBySubCategory.admin,
                role: "org_admin",
            });
        }
        // process groups
        const groupIdsBySubCategory = getNonOwnerAclKeysByCategory(aclCategory_1.AclCategory.GROUP, channel);
        // group members
        if (groupIdsBySubCategory.member.length) {
            filter.predicates.push({ group: groupIdsBySubCategory.member });
        }
        // group admins
        if (groupIdsBySubCategory.admin.length) {
            // the /community/users endpoint supports searching for users that are members of specific groups via `group`, but there's no way
            // to further refine results to a specific type of membership within that group (e.g. memberType:admin). to do so, we need to fetch the
            // group members via /community/groups/:id/users for each group and then build the appropriate predicates. however, given that group
            // owners & admins will be returned in the results for the member predicate above, we only need to do this for groups whose ID is not
            // also present in the above member predicate
            const adminOnlyGroupIds = groupIdsBySubCategory.admin.filter((groupId) => !groupIdsBySubCategory.member.includes(groupId));
            if (adminOnlyGroupIds.length) {
                // fetch the member list for each channel group
                const groupMemberships = await Promise.all(adminOnlyGroupIds.map((groupId) => 
                // catch & resolve null when an error occurs. depending on the group's visibility settings, the
                // user may, or may not be able to access the group's user list.
                (0, arcgis_rest_portal_1.getGroupUsers)(groupId, requestOptions).catch((_e) => null)));
                // build the appropriate predicate for each group
                groupMemberships.forEach((groupUserResult, idx) => {
                    // only if the user can access the group's user list
                    if (groupUserResult) {
                        filter.predicates.push({
                            group: adminOnlyGroupIds[idx],
                            // restrict results to the group owner and managers/admins
                            username: [
                                groupUserResult.owner,
                                ...groupUserResult.admins,
                            ].filter(util_1.unique),
                        });
                    }
                });
            }
        }
        filters.push(filter);
    }
    // if the user is authenticated, omit that user from the results
    if (currentUsername) {
        filters.push({
            operation: "AND",
            predicates: [{ username: { not: currentUsername } }],
        });
    }
    // conditionally add terms
    const inputFilter = terms.reduce((acc, term) => term
        ? Object.assign(Object.assign({}, acc), { predicates: [
                ...acc.predicates,
                { username: term },
                { fullname: term },
            ] }) : acc, {
        operation: "OR",
        predicates: [],
    });
    if (inputFilter.predicates.length) {
        filters.push(inputFilter);
    }
    return {
        targetEntity: "communityUser",
        filters,
    };
}
/**
 * Transforms a given channel and optional channel groups array into a IHubSearchResult
 * @param channel
 * @param groups
 * @returns
 */
const channelToSearchResult = (channel, groups) => {
    return Object.assign(Object.assign({}, channel), { id: channel.id, name: channel.name, createdDate: new Date(channel.createdAt), createdDateSource: "channel", updatedDate: new Date(channel.updatedAt), updatedDateSource: "channel", type: "channel", access: getChannelAccess(channel), family: "channel", owner: channel.creator, links: {
            // TODO: add links?
            thumbnail: null,
            self: null,
            siteRelative: null,
        }, includes: { groups }, rawResult: channel });
};
exports.channelToSearchResult = channelToSearchResult;
/**
 * Constructs file name for exported csvs
 * @param entityTitle
 * @returns string
 */
function getPostCSVFileName(entityTitle) {
    const et = entityTitle;
    const suffix = `_${new Date(Date.now())
        .toISOString()
        .replace(/[^a-z0-9]/gi, "-")}.csv`;
    const prefix = et
        // coerce to lower case
        .toLowerCase()
        // replace non-alpha-numeric chars with hyphens
        .replace(/[^a-z0-9]/g, "-")
        // replace consecutive hyphens with single hyphen
        .replace(/-{2,}/g, "-")
        // replace leading hyphens with empty string
        .replace(/^-/, "")
        // truncate filename so it doesn't exceed 250 chars to avoid OS filename length limitations
        .substring(0, 250 - suffix.length)
        // replace trailing hyphens
        .replace(/-$/, "");
    return [prefix, suffix].join("");
}
//# sourceMappingURL=utils.js.map