/**
 * Per-user admin read models for the User detail drawer (Overview / Security /
 * Sessions / Activity / Permissions tabs).
 *
 * Pure data shaping over the Prisma client — no config or request coupling — so
 * it unit-tests cleanly. The API layer supplies `workspaceDomain` (for the
 * external-domain flag) and the viewer's `currentSessionId` (to mark the
 * "this device" session). Mirrors the conventions in `team.ts`.
 */
import { toDisplayRole, permissionsForRole, } from './roles.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
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';
}
/**
 * Best-effort User-Agent parsing into a friendly device label. Intentionally
 * dependency-free and conservative: an unknown UA degrades to "Unknown device"
 * rather than guessing. This is presentational only — never used for auth.
 */
export function parseUserAgent(ua) {
    if (!ua)
        return { browser: 'Unknown', os: 'Unknown', deviceLabel: 'Unknown device' };
    let browser = 'Unknown';
    // Order matters: Edge/Chrome both contain "Chrome"; check the more specific first.
    if (/\bEdg(e|A|iOS)?\//.test(ua))
        browser = 'Edge';
    else if (/\bOPR\/|\bOpera\b/.test(ua))
        browser = 'Opera';
    else if (/\bChrome\//.test(ua) && !/\bChromium\b/.test(ua))
        browser = 'Chrome';
    else if (/\bChromium\//.test(ua))
        browser = 'Chromium';
    else if (/\bFirefox\//.test(ua))
        browser = 'Firefox';
    else if (/\bVersion\/.*Safari\//.test(ua) || /\bSafari\//.test(ua))
        browser = 'Safari';
    let os = 'Unknown';
    if (/\bWindows NT 10/.test(ua))
        os = 'Windows';
    else if (/\bWindows\b/.test(ua))
        os = 'Windows';
    else if (/\biPhone\b/.test(ua))
        os = 'iPhone';
    else if (/\biPad\b/.test(ua))
        os = 'iPad';
    else if (/\bMac OS X\b|\bMacintosh\b/.test(ua))
        os = 'macOS';
    else if (/\bAndroid\b/.test(ua))
        os = 'Android';
    else if (/\bLinux\b/.test(ua))
        os = 'Linux';
    const deviceLabel = browser === 'Unknown' && os === 'Unknown' ? 'Unknown device' : `${browser} on ${os}`;
    return { browser, os, deviceLabel };
}
// ---------------------------------------------------------------------------
// Detail
// ---------------------------------------------------------------------------
export async function getUserDetail(db, userId, opts = {}) {
    const user = await db.user.findUnique({ where: { id: userId } });
    if (!user)
        return null;
    // OAuth providers connected to this account (best-effort; model may be absent
    // in trimmed test doubles).
    let oauthProviders = [];
    if (db.oAuthAccount?.findMany) {
        try {
            const rows = await db.oAuthAccount.findMany({
                where: { userId },
                select: { provider: true },
            });
            oauthProviders = Array.from(new Set(rows.map((r) => String(r.provider)).filter(Boolean)));
        }
        catch {
            oauthProviders = [];
        }
    }
    const contentOwnedCount = await countOwnedContent(db, userId);
    let invitedBy = null;
    if (user.invitedById && db.user?.findUnique) {
        try {
            const inviter = await db.user.findUnique({
                where: { id: user.invitedById },
                select: { id: true, name: true, email: true },
            });
            if (inviter)
                invitedBy = { id: inviter.id, name: inviter.name, email: inviter.email };
        }
        catch {
            invitedBy = null;
        }
    }
    const passwordSet = !!user.passwordHash;
    const domain = emailDomain(user.email);
    return {
        id: user.id,
        name: user.name,
        email: user.email,
        avatarUrl: user.avatarUrl ?? null,
        initials: initialsFor(user.name, user.email),
        role: user.role,
        roleName: toDisplayRole(user.role),
        status: memberStatus(user),
        mfaEnabled: !!user.totpEnabled,
        mfaRequired: !!user.mfaRequired,
        passwordSet,
        forcePasswordReset: !!user.forcePasswordReset,
        failedLoginCount: user.failedLoginCount ?? 0,
        lockedAt: iso(user.lockedAt),
        suspendedAt: iso(user.suspendedAt),
        authProvider: user.authProvider ?? null,
        oauthProviders,
        ssoOnly: !passwordSet && oauthProviders.length > 0,
        emailVerified: !!user.emailVerified,
        lastLoginAt: iso(user.lastLoginAt),
        lastActiveAt: iso(user.lastActiveAt ?? user.lastLoginAt),
        acceptedInviteAt: iso(user.acceptedInviteAt),
        createdAt: iso(user.createdAt) ?? new Date().toISOString(),
        invitedBy,
        externalDomain: !!opts.workspaceDomain && !!domain && domain !== opts.workspaceDomain,
        contentOwnedCount,
        isCurrentUser: !!opts.currentUserId && user.id === opts.currentUserId,
    };
}
// ---------------------------------------------------------------------------
// Sessions
// ---------------------------------------------------------------------------
export async function listUserSessions(db, userId, currentSessionId) {
    if (!db.session?.findMany)
        return [];
    const rows = await db.session.findMany({
        where: { userId },
        orderBy: { createdAt: 'desc' },
    });
    const now = Date.now();
    return rows.map((s) => {
        const { browser, os, deviceLabel } = parseUserAgent(s.userAgent);
        const expiresAt = iso(s.expiresAt) ?? new Date().toISOString();
        const revokedAt = iso(s.revokedAt);
        const active = !revokedAt && new Date(expiresAt).getTime() > now;
        return {
            id: s.id,
            deviceLabel,
            browser,
            os,
            ipCountry: null,
            createdAt: iso(s.createdAt) ?? new Date().toISOString(),
            lastSeenAt: iso(s.createdAt) ?? new Date().toISOString(),
            expiresAt,
            revokedAt,
            current: !!currentSessionId && s.id === currentSessionId,
            active,
        };
    });
}
// ---------------------------------------------------------------------------
// Activity
// ---------------------------------------------------------------------------
export async function listUserActivity(db, userId, limit = 50) {
    if (!db.auditLog?.findMany)
        return [];
    // Events where the user is the actor, plus events targeting the user (these
    // store the subject in `details.targetUserId`). Two queries merged rather than
    // a JSON-path OR so it works against simple test doubles and any DB.
    const take = Math.min(Math.max(limit, 1), 200);
    const [asActor, asTarget] = await Promise.all([
        db.auditLog.findMany({
            where: { userId },
            orderBy: { timestamp: 'desc' },
            take,
        }),
        db.auditLog
            .findMany({
            where: { details: { path: ['targetUserId'], equals: userId } },
            orderBy: { timestamp: 'desc' },
            take,
        })
            .catch(() => []),
    ]);
    const seen = new Set();
    const merged = [];
    for (const r of asActor) {
        if (seen.has(r.id))
            continue;
        seen.add(r.id);
        merged.push(toActivity(r, 'actor'));
    }
    for (const r of asTarget) {
        if (seen.has(r.id))
            continue;
        seen.add(r.id);
        merged.push(toActivity(r, 'target'));
    }
    merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
    return merged.slice(0, take);
}
function toActivity(r, role) {
    return {
        id: r.id,
        event: r.event,
        role,
        ipAddress: r.ipAddress ?? null,
        timestamp: iso(r.timestamp) ?? new Date().toISOString(),
        details: r.details ?? null,
    };
}
// ---------------------------------------------------------------------------
// Permissions
// ---------------------------------------------------------------------------
export function getUserPermissionView(role) {
    return {
        role: role?.toUpperCase() ?? 'VIEWER',
        roleName: toDisplayRole(role),
        permissions: permissionsForRole(role),
    };
}
/**
 * Count the content a user *owns* (authored documents, uploaded media,
 * created templates). Soft-deleted documents are included so the count
 * reflects everything a transfer would reassign — nothing is silently
 * orphaned. Immutable history (versions, audit logs) is never counted.
 */
export async function countOwnedContent(db, userId) {
    const safeCount = async (model, where) => {
        if (!model?.count)
            return 0;
        try {
            return await model.count({ where });
        }
        catch {
            return 0;
        }
    };
    const [documents, media, templates] = await Promise.all([
        safeCount(db.document, { createdById: userId }),
        safeCount(db.media, { uploadedById: userId }),
        safeCount(db.contentTemplate, { createdById: userId }),
    ]);
    return documents + media + templates;
}
/**
 * Reassign every piece of content owned by `fromUserId` to `toUserId`:
 * authored documents (`Document.createdById`), uploaded media
 * (`Media.uploadedById`), and created templates (`ContentTemplate.createdById`).
 *
 * Deliberately does NOT touch `updatedById`, version history, or audit logs —
 * those record *who did what* and rewriting them would falsify the trail. This
 * only moves the "owner of record" so content survives a user's removal.
 *
 * Returns the per-type counts actually reassigned. Each model is guarded so a
 * trimmed Prisma schema (or test double) that lacks one of them is a no-op for
 * that type rather than a hard failure.
 */
export async function transferContentOwnership(db, fromUserId, toUserId) {
    const reassign = async (model, field) => {
        if (!model?.updateMany)
            return 0;
        const res = await model.updateMany({
            where: { [field]: fromUserId },
            data: { [field]: toUserId },
        });
        return typeof res?.count === 'number' ? res.count : 0;
    };
    const documents = await reassign(db.document, 'createdById');
    const media = await reassign(db.media, 'uploadedById');
    const templates = await reassign(db.contentTemplate, 'createdById');
    return { documents, media, templates, total: documents + media + templates };
}
//# sourceMappingURL=user-admin.js.map