/**
 * Team / access-control read models for the Users admin tab.
 *
 * Pure data shaping over the Prisma client (no config or request coupling) so it
 * unit-tests cleanly. The API layer derives `workspaceDomain` / `seatLimit` from
 * config and passes them in.
 *
 *   - `listTeamMembers`  — active/suspended/locked members, de-duplicated against
 *                          pending invites (a placeholder user for a pending
 *                          invite is shown only as the invite row).
 *   - `getUserStats`     — the four summary cards (members, pending, admins, seats).
 *   - `computeAccessReview` — risk recommendations from real signals.
 */
import { toDisplayRole } from './roles.js';
const INACTIVE_DAYS = 90;
const STALE_INVITE_DAYS = 7;
const DAY_MS = 24 * 60 * 60 * 1000;
function initialsFor(name, email) {
    const source = name?.trim() || email;
    const parts = source.split(/\s+/).filter(Boolean);
    if (parts.length >= 2)
        return (parts[0][0] + parts[1][0]).toUpperCase();
    const single = parts[0] ?? email;
    return single.slice(0, 2).toUpperCase();
}
function emailDomain(email) {
    return email.split('@')[1]?.toLowerCase() ?? '';
}
function iso(d) {
    if (!d)
        return null;
    return d instanceof Date ? d.toISOString() : d;
}
function memberStatus(user) {
    if (!user.isActive)
        return 'deactivated';
    if (user.lockedAt)
        return 'locked';
    if (user.suspendedAt)
        return 'suspended';
    return 'active';
}
function toMember(user, opts) {
    const domain = emailDomain(user.email);
    return {
        id: user.id,
        name: user.name,
        email: user.email,
        initials: initialsFor(user.name, user.email),
        role: user.role,
        roleName: toDisplayRole(user.role),
        status: memberStatus(user),
        mfaEnabled: !!user.totpEnabled,
        mfaRequired: !!user.mfaRequired,
        externalDomain: !!opts.workspaceDomain && !!domain && domain !== opts.workspaceDomain,
        authProvider: user.authProvider ?? null,
        lastLoginAt: iso(user.lastLoginAt),
        lastActiveAt: iso(user.lastActiveAt ?? user.lastLoginAt),
        acceptedInviteAt: iso(user.acceptedInviteAt),
        createdAt: iso(user.createdAt) ?? new Date().toISOString(),
        isCurrentUser: !!opts.currentUserId && user.id === opts.currentUserId,
    };
}
/** Emails with a still-PENDING invite — excluded from the member list. */
async function pendingInviteEmails(db) {
    if (!db.invite?.findMany)
        return new Set();
    const rows = await db.invite.findMany({
        where: { status: 'PENDING' },
        select: { email: true },
    });
    return new Set(rows.map((r) => r.email.toLowerCase()));
}
export async function listTeamMembers(db, filter = {}, opts = {}) {
    const [users, pending] = await Promise.all([
        db.user.findMany({ orderBy: { createdAt: 'asc' } }),
        pendingInviteEmails(db),
    ]);
    let members = users
        .filter((u) => !pending.has(u.email.toLowerCase()))
        .map((u) => toMember(u, opts));
    if (filter.role && filter.role.toLowerCase() !== 'all') {
        const want = filter.role.toLowerCase();
        members = members.filter((m) => m.roleName.toLowerCase() === want);
    }
    if (filter.status && filter.status.toLowerCase() !== 'all') {
        const want = filter.status.toLowerCase();
        members = members.filter((m) => m.status === want);
    }
    if (filter.search) {
        const q = filter.search.trim().toLowerCase();
        members = members.filter((m) => m.name.toLowerCase().includes(q) ||
            m.email.toLowerCase().includes(q) ||
            m.roleName.toLowerCase().includes(q) ||
            m.status.includes(q) ||
            emailDomain(m.email).includes(q));
    }
    return members;
}
export async function getUserStats(db, opts = {}) {
    const members = await listTeamMembers(db, {}, opts);
    const active = members.filter((m) => m.status === 'active');
    const admins = active.filter((m) => m.roleName === 'Owner' || m.roleName === 'Admin').length;
    let pendingInvites = 0;
    if (db.invite?.count) {
        pendingInvites = await db.invite.count({
            where: { status: 'PENDING', expiresAt: { gt: new Date() } },
        });
    }
    // Active members + pending invites reserve seats. Suspended/locked/deactivated
    // do not count.
    const seatsUsed = active.length + pendingInvites;
    const seatLimit = opts.seatLimit ?? null;
    const seatsAvailable = seatLimit == null ? null : Math.max(0, seatLimit - seatsUsed);
    return {
        members: active.length,
        pendingInvites,
        admins,
        seatsUsed,
        seatLimit,
        seatsAvailable,
    };
}
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
function daysSince(d) {
    if (!d)
        return null;
    return Math.floor((Date.now() - new Date(d).getTime()) / DAY_MS);
}
/**
 * Build one primary recommendation per member (highest-risk signal), plus a
 * recommendation per stale pending invite. Dismissed ids are filtered out and
 * the result is sorted highest-severity first.
 */
export function buildRecommendations(members, pendingInvites, dismissed) {
    const recs = [];
    for (const m of members) {
        if (m.status !== 'active')
            continue;
        const inactiveDays = daysSince(m.lastLoginAt);
        const inactive = inactiveDays != null && inactiveDays >= INACTIVE_DAYS;
        const neverSignedIn = m.lastLoginAt == null && m.acceptedInviteAt != null;
        const isOwner = m.roleName === 'Owner';
        const isAdmin = m.roleName === 'Admin';
        let rec = null;
        if (isOwner && !m.mfaEnabled) {
            rec = {
                id: `owner_without_mfa:${m.id}`,
                userId: m.id,
                severity: 'critical',
                type: 'owner_without_mfa',
                title: 'Owner without MFA',
                description: `${m.name} (${m.email}) is an Owner but has not enabled multi-factor authentication.`,
                recommendedAction: 'Require MFA',
                status: 'open',
            };
        }
        else if (isAdmin && !m.mfaEnabled) {
            rec = {
                id: `admin_without_mfa:${m.id}`,
                userId: m.id,
                severity: 'high',
                type: 'admin_without_mfa',
                title: 'Admin without MFA',
                description: `${m.name} (${m.email}) has admin access but has not enabled multi-factor authentication.`,
                recommendedAction: 'Require MFA',
                status: 'open',
            };
        }
        else if (inactive && m.externalDomain) {
            rec = {
                id: `inactive_user:${m.id}`,
                userId: m.id,
                severity: 'high',
                type: 'inactive_user',
                title: 'Access review',
                description: `${m.name} (${m.email}) hasn't signed in for ${inactiveDays} days and holds an external address. Consider revoking access to reduce risk.`,
                recommendedAction: 'Revoke access',
                status: 'open',
            };
        }
        else if (inactive) {
            rec = {
                id: `inactive_user:${m.id}`,
                userId: m.id,
                severity: 'medium',
                type: 'inactive_user',
                title: 'Inactive user',
                description: `${m.name} (${m.email}) hasn't signed in for ${inactiveDays} days.`,
                recommendedAction: 'Revoke access',
                status: 'open',
            };
        }
        else if (m.externalDomain) {
            rec = {
                id: `external_domain:${m.id}`,
                userId: m.id,
                severity: m.roleName === 'Viewer' ? 'low' : 'medium',
                type: 'external_domain',
                title: 'External domain',
                description: `${m.name} (${m.email}) signs in with an external email domain.`,
                recommendedAction: 'Review access',
                status: 'open',
            };
        }
        else if (neverSignedIn) {
            rec = {
                id: `never_signed_in:${m.id}`,
                userId: m.id,
                severity: 'medium',
                type: 'never_signed_in',
                title: 'Never signed in',
                description: `${m.name} (${m.email}) has an account but has never signed in.`,
                recommendedAction: 'Send reminder',
                status: 'open',
            };
        }
        if (rec && !dismissed.has(rec.id))
            recs.push(rec);
    }
    for (const inv of pendingInvites) {
        const age = daysSince(inv.createdAt);
        if (age != null && age >= STALE_INVITE_DAYS) {
            const id = `stale_invite:${inv.id}`;
            if (!dismissed.has(id)) {
                recs.push({
                    id,
                    userId: null,
                    inviteId: inv.id,
                    severity: 'medium',
                    type: 'stale_invite',
                    title: 'Stale invitation',
                    description: `The invitation to ${inv.email} has been pending for ${age} days.`,
                    recommendedAction: 'Resend or cancel',
                    status: 'open',
                });
            }
        }
    }
    return recs.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
}
export async function computeAccessReview(db, opts = {}) {
    const members = await listTeamMembers(db, {}, opts);
    let pending = [];
    if (db.invite?.findMany) {
        const rows = await db.invite.findMany({
            where: { status: 'PENDING' },
            select: { id: true, email: true, createdAt: true },
        });
        pending = rows.map((r) => ({
            id: r.id,
            email: r.email,
            createdAt: iso(r.createdAt) ?? new Date().toISOString(),
        }));
    }
    return buildRecommendations(members, pending, new Set(opts.dismissedIds ?? []));
}
/** Derive the registrable workspace domain from a site URL, if any. */
export function workspaceDomainFromUrl(siteUrl) {
    if (!siteUrl)
        return null;
    try {
        const host = new URL(siteUrl).hostname.toLowerCase();
        return host.replace(/^www\./, '');
    }
    catch {
        return null;
    }
}
//# sourceMappingURL=team.js.map