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;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import DatabaseConfig from "../DatabaseConfig";
import { IsBillingEnabled } from "../EnvironmentConfig";
import QueryHelper from "../Types/Database/QueryHelper";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import Errors from "../Utils/Errors";
import logger from "../Utils/Logger";
import AccessTokenService from "./AccessTokenService";
import BillingService from "./BillingService";
import DatabaseService from "./DatabaseService";
import MailService from "./MailService";
import ProjectService from "./ProjectService";
import UserNotificationRuleService from "./UserNotificationRuleService";
import UserNotificationSettingService from "./UserNotificationSettingService";
import UserService from "./UserService";
import { AccountsRoute } from "../../ServiceRoute";
import URL from "../../Types/API/URL";
import Route from "../../Types/API/Route";
import SubscriptionPlan, { PlanType, } from "../../Types/Billing/SubscriptionPlan";
import LIMIT_MAX from "../../Types/Database/LimitMax";
import Email from "../../Types/Email";
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
import BadDataException from "../../Types/Exception/BadDataException";
import ObjectID from "../../Types/ObjectID";
import TeamMember from "../../Models/DatabaseModels/TeamMember";
import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
import OneUptimeDate from "../../Types/Date";
import ProjectSCIMService from "./ProjectSCIMService";
import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
export class TeamMemberService extends DatabaseService {
    constructor() {
        super(TeamMember);
        /*
         * Caches the user's accepted team memberships per project. Auth middleware
         * calls this on every authenticated request to evaluate the `Owned`
         * permission scope; without the cache it's a Postgres findBy per request.
         * 60s of staleness on team membership changes is acceptable; we also
         * invalidate proactively when team membership writes happen.
         */
        this.teamIdsForUserCache = new InMemoryTTLCache(10000);
    }
    async isSCIMPushGroupsEnabled(projectId) {
        const count = await ProjectSCIMService.countBy({
            query: {
                projectId: projectId,
                enablePushGroups: true,
            },
            props: {
                isRoot: true,
            },
        });
        return count.toNumber() > 0;
    }
    async onBeforeCreate(createBy) {
        // Check if SCIM is enabled for the project
        if (!createBy.props.isRoot &&
            (await this.isSCIMPushGroupsEnabled(createBy.data.projectId || createBy.props.tenantId))) {
            throw new BadDataException("Cannot invite team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.");
        }
        // check if this project can have more members.
        if (IsBillingEnabled && createBy.data.projectId) {
            const project = await ProjectService.findOneById({
                id: createBy.data.projectId,
                select: {
                    seatLimit: true,
                    paymentProviderSubscriptionSeats: true,
                },
                props: {
                    isRoot: true,
                },
            });
            if (project &&
                project.seatLimit &&
                project.paymentProviderSubscriptionSeats &&
                project.paymentProviderSubscriptionSeats >= project.seatLimit) {
                throw new BadDataException(Errors.TeamMemberService.LIMIT_REACHED);
            }
            if (createBy.props.currentPlan === PlanType.Free &&
                project &&
                project.paymentProviderSubscriptionSeats &&
                project.paymentProviderSubscriptionSeats >= 1) {
                throw new BadDataException(Errors.TeamMemberService.LIMIT_REACHED_FOR_FREE_PLAN);
            }
        }
        if (!createBy.props.isRoot) {
            createBy.data.hasAcceptedInvitation = false;
        }
        if (createBy.miscDataProps && createBy.miscDataProps["email"]) {
            const email = new Email(createBy.miscDataProps["email"]);
            let user = await UserService.findByEmail(email, {
                isRoot: true,
            });
            let isNewUser = false;
            if (!user) {
                isNewUser = true;
                user = await UserService.createByEmail({
                    email,
                    name: undefined, // name is not required for now.
                    props: {
                        isRoot: true,
                    },
                });
            }
            createBy.data.userId = user.id;
            const project = await ProjectService.findOneById({
                id: createBy.data.projectId,
                select: {
                    name: true,
                },
                props: {
                    isRoot: true,
                },
            });
            if (project) {
                const host = await DatabaseConfig.getHost();
                const httpProtocol = await DatabaseConfig.getHttpProtocol();
                MailService.sendMail({
                    toEmail: email,
                    templateType: EmailTemplateType.InviteMember,
                    vars: {
                        signInLink: URL.fromString(new URL(httpProtocol, host, new Route(AccountsRoute.toString())).toString()).toString(),
                        registerLink: URL.fromString(new URL(httpProtocol, host, new Route(AccountsRoute.toString())).toString())
                            .addRoute("/register")
                            .addQueryParam("email", email.toString(), true)
                            .toString(),
                        isNewUser: isNewUser.toString(),
                        projectName: project.name,
                        homeUrl: new URL(httpProtocol, host).toString(),
                    },
                    subject: "You have been invited to " + project.name,
                }, {
                    projectId: createBy.data.projectId,
                    userId: user.id,
                }).catch((err) => {
                    var _a, _b;
                    logger.error(err, {
                        projectId: (_a = createBy.data.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
                        userId: (_b = user === null || user === void 0 ? void 0 : user.id) === null || _b === void 0 ? void 0 : _b.toString(),
                    });
                });
            }
        }
        //check if this user is already invited.
        const member = await this.findOneBy({
            query: {
                userId: createBy.data.userId,
                teamId: createBy.data.teamId || new ObjectID(createBy.data.team._id),
            },
            props: {
                isRoot: true,
            },
            select: {
                _id: true,
            },
        });
        if (member) {
            throw new BadDataException(Errors.TeamMemberService.ALREADY_INVITED);
        }
        return { createBy, carryForward: null };
    }
    async refreshTokens(userId, projectId) {
        /*
         * Invalidate the in-process cache of this user's team memberships in
         * this project — membership just changed.
         */
        this.teamIdsForUserCache.delete(`${userId.toString()}:${projectId.toString()}`);
        /// Refresh tokens.
        await AccessTokenService.refreshUserGlobalAccessPermission(userId);
        await AccessTokenService.refreshUserTenantAccessPermission(userId, projectId);
    }
    async onCreateSuccess(onCreate, createdItem) {
        await this.refreshTokens(onCreate.createBy.data.userId, onCreate.createBy.data.projectId);
        await this.updateSubscriptionSeatsByUniqueTeamMembersInProject(onCreate.createBy.data.projectId);
        return createdItem;
    }
    async onUpdateSuccess(onUpdate, updatedItemIds) {
        var _a, _b;
        const updateBy = onUpdate.updateBy;
        const items = await this.findBy({
            query: {
                _id: QueryHelper.any(updatedItemIds),
            },
            select: {
                userId: true,
                user: {
                    email: true,
                    isEmailVerified: true,
                },
                projectId: true,
            },
            limit: LIMIT_MAX,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        for (const item of items) {
            await this.refreshTokens(item.userId, item.projectId);
            if (updateBy.data.hasAcceptedInvitation && ((_a = item.user) === null || _a === void 0 ? void 0 : _a.isEmailVerified)) {
                await UserNotificationSettingService.addDefaultNotificationSettingsForUser(item.userId, item.projectId);
                await UserNotificationRuleService.addDefaultNotificationRuleForUser(item.projectId, item.userId, (_b = item.user) === null || _b === void 0 ? void 0 : _b.email);
            }
        }
        return { updateBy, carryForward: onUpdate.carryForward };
    }
    async onBeforeDelete(deleteBy) {
        var _a, _b;
        const members = await this.findBy({
            query: deleteBy.query,
            select: {
                userId: true,
                projectId: true,
                teamId: true,
                hasAcceptedInvitation: true,
                team: {
                    _id: true,
                    shouldHaveAtLeastOneMember: true,
                },
            },
            limit: LIMIT_MAX,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        // Check if SCIM is enabled for the project
        if (
        // check if not root.
        !deleteBy.props.isRoot &&
            members.length > 0 &&
            ((_a = members[0]) === null || _a === void 0 ? void 0 : _a.projectId) &&
            (await this.isSCIMPushGroupsEnabled(members[0].projectId))) {
            throw new BadDataException("Cannot delete team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.");
        }
        // check if there's one member in the team.
        for (const member of members) {
            OnCallDutyPolicyTimeLogService.endTimeForUser({
                projectId: member.projectId,
                userId: member.userId,
                endsAt: OneUptimeDate.getCurrentDate(),
            }).catch((err) => {
                var _a, _b;
                logger.error(err, {
                    projectId: (_a = member.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
                    userId: (_b = member.userId) === null || _b === void 0 ? void 0 : _b.toString(),
                });
            });
            if ((_b = member.team) === null || _b === void 0 ? void 0 : _b.shouldHaveAtLeastOneMember) {
                if (!member.hasAcceptedInvitation) {
                    continue;
                }
                const membersInTeam = await this.countBy({
                    query: {
                        teamId: member.teamId,
                        hasAcceptedInvitation: true,
                    },
                    skip: 0,
                    limit: LIMIT_MAX,
                    props: {
                        isRoot: true,
                    },
                });
                // Skip the one-member guard when SCIM manages membership for the project.
                const isPushGroupsManaged = await this.isSCIMPushGroupsEnabled(member.projectId);
                if (!isPushGroupsManaged && membersInTeam.toNumber() <= 1) {
                    throw new BadDataException(Errors.TeamMemberService.ONE_MEMBER_REQUIRED);
                }
            }
        }
        return {
            deleteBy: deleteBy,
            carryForward: members,
        };
    }
    async onDeleteSuccess(onDelete) {
        for (const item of onDelete.carryForward) {
            await this.refreshTokens(item.userId, item.projectId);
            await this.updateSubscriptionSeatsByUniqueTeamMembersInProject(item.projectId);
            await UserNotificationSettingService.removeDefaultNotificationSettingsForUser(item.userId, item.projectId);
        }
        return onDelete;
    }
    async getUniqueTeamMemberCountInProject(projectId) {
        const members = await this.findBy({
            query: {
                projectId: projectId,
            },
            props: {
                isRoot: true,
            },
            select: {
                userId: true,
            },
            skip: 0,
            limit: LIMIT_MAX,
        });
        const memberIds = members
            .map((member) => {
            var _a;
            return (_a = member.userId) === null || _a === void 0 ? void 0 : _a.toString();
        })
            .filter((memberId) => {
            return Boolean(memberId);
        });
        return [...new Set(memberIds)].length; //get unique member ids.
    }
    async getUsersInTeams(teamIds) {
        const members = await this.findBy({
            query: {
                teamId: QueryHelper.any(teamIds),
            },
            props: {
                isRoot: true,
            },
            select: {
                _id: true,
                user: {
                    _id: true,
                    email: true,
                    name: true,
                    timezone: true,
                },
            },
            skip: 0,
            limit: LIMIT_MAX,
        });
        const uniqueUserIds = new Set();
        const uniqueMembers = members.filter((member) => {
            var _a, _b;
            const userId = (_b = (_a = member.user) === null || _a === void 0 ? void 0 : _a._id) === null || _b === void 0 ? void 0 : _b.toString();
            if (userId && !uniqueUserIds.has(userId)) {
                uniqueUserIds.add(userId);
                return true;
            }
            return false;
        });
        return uniqueMembers.map((member) => {
            return member.user;
        });
    }
    async getUsersInTeam(teamId) {
        const members = await this.findBy({
            query: {
                teamId: teamId,
            },
            props: {
                isRoot: true,
            },
            select: {
                _id: true,
                user: {
                    _id: true,
                    email: true,
                    name: true,
                },
            },
            skip: 0,
            limit: LIMIT_MAX,
        });
        return members.map((member) => {
            return member.user;
        });
    }
    async updateSubscriptionSeatsByUniqueTeamMembersInProject(projectId) {
        if (!IsBillingEnabled) {
            return;
        }
        const numberOfMembers = await this.getUniqueTeamMemberCountInProject(projectId);
        const project = await ProjectService.findOneById({
            id: projectId,
            select: {
                paymentProviderSubscriptionId: true,
                paymentProviderPlanId: true,
            },
            props: {
                isRoot: true,
            },
        });
        if (project &&
            project.paymentProviderSubscriptionId &&
            (project === null || project === void 0 ? void 0 : project.paymentProviderPlanId)) {
            const plan = SubscriptionPlan.getSubscriptionPlanById(project === null || project === void 0 ? void 0 : project.paymentProviderPlanId);
            if (!plan) {
                return;
            }
            await BillingService.changeQuantity(project.paymentProviderSubscriptionId, numberOfMembers);
            await ProjectService.updateOneById({
                id: projectId,
                data: {
                    paymentProviderSubscriptionSeats: numberOfMembers,
                },
                props: {
                    isRoot: true,
                },
            });
        }
    }
    /*
     * Returns the IDs of teams the given user has accepted membership in,
     * scoped to a single project. Used by the `Owned` permission scope to
     * resolve "any of the user's teams owns this resource."
     */
    async getTeamIdsForUser(userId, projectId) {
        const cacheKey = `${userId.toString()}:${projectId.toString()}`;
        const cached = this.teamIdsForUserCache.get(cacheKey);
        if (cached !== undefined) {
            return cached.map((id) => {
                return new ObjectID(id);
            });
        }
        const members = await this.findBy({
            query: {
                userId: userId,
                projectId: projectId,
                hasAcceptedInvitation: true,
            },
            props: {
                isRoot: true,
            },
            select: {
                teamId: true,
            },
            skip: 0,
            limit: LIMIT_MAX,
        });
        const teamIds = [];
        const seen = new Set();
        for (const member of members) {
            const id = member.teamId;
            if (id && !seen.has(id.toString())) {
                seen.add(id.toString());
                teamIds.push(id);
            }
        }
        this.teamIdsForUserCache.set(cacheKey, teamIds.map((id) => {
            return id.toString();
        }), 60000);
        return teamIds;
    }
}
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "isSCIMPushGroupsEnabled", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "onBeforeCreate", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID,
        ObjectID]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "refreshTokens", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, TeamMember]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "onCreateSuccess", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Array]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "onUpdateSuccess", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "onBeforeDelete", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "onDeleteSuccess", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "getUniqueTeamMemberCountInProject", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Array]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "getUsersInTeams", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "getUsersInTeam", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "updateSubscriptionSeatsByUniqueTeamMembersInProject", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID,
        ObjectID]),
    __metadata("design:returntype", Promise)
], TeamMemberService.prototype, "getTeamIdsForUser", null);
export default new TeamMemberService();
//# sourceMappingURL=TeamMemberService.js.map