import path from "node:path";
import { ADMIN_GROUP_ID, BUILTIN_GROUPS, READONLY_GROUP_ID, isPermission, } from "./access-control.js";
import { isArtifactDeliveryMode } from "../artifacts/artifact-delivery.js";
export function normalizePayload(payload) {
    const now = new Date().toISOString();
    const groupsById = new Map();
    for (const group of BUILTIN_GROUPS) {
        groupsById.set(group.id, {
            ...group,
            permissions: group.id === ADMIN_GROUP_ID ? allPermissionsSafe() : group.permissions,
            agentIds: [],
            workspaceRoots: [],
            telegramChatIds: [],
            discordChannelIds: [],
            slackChannelIds: [],
            matrixRoomIds: [],
            peerIds: [],
            createdAt: now,
            updatedAt: now,
        });
    }
    for (const group of payload?.groups ?? []) {
        if (!isGroupRecord(group))
            continue;
        groupsById.set(group.id, {
            ...group,
            permissions: group.id === ADMIN_GROUP_ID ? allPermissionsSafe() : normalizePermissions(group.permissions),
            system: BUILTIN_GROUPS.some((builtin) => builtin.id === group.id) || group.system,
            agentIds: normalizeStringList(group.agentIds),
            workspaceRoots: normalizeStringList(group.workspaceRoots),
            telegramChatIds: normalizeNumberList(group.telegramChatIds),
            discordChannelIds: normalizeStringList(group.discordChannelIds),
            slackChannelIds: normalizeStringList(group.slackChannelIds),
            matrixRoomIds: normalizeStringList(group.matrixRoomIds),
            peerIds: normalizeStringList(group.peerIds),
        });
    }
    const groups = Array.from(groupsById.values());
    const groupIds = new Set(groups.map((group) => group.id));
    const users = (payload?.users ?? []).filter(isUserRecord).map((user) => ({
        ...user,
        preferences: normalizeUserPreferences(user.preferences),
    }));
    const userIds = new Set(users.map((user) => user.id));
    return {
        version: 1,
        users,
        groups,
        userGroups: (payload?.userGroups ?? []).filter((item) => isUserGroupRecord(item) && userIds.has(item.userId) && groupIds.has(item.groupId)),
        telegramIdentities: (payload?.telegramIdentities ?? []).filter((item) => isTelegramIdentityRecord(item) && userIds.has(item.userId)),
        telegramChats: (payload?.telegramChats ?? []).filter(isTelegramChatAccessRecord).map((chat) => ({
            ...chat,
            artifactDelivery: normalizeArtifactDeliveryMode(chat.artifactDelivery),
            allowedGroupIds: chat.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
        })),
        discordIdentities: (payload?.discordIdentities ?? []).filter((item) => isDiscordIdentityRecord(item) && userIds.has(item.userId)),
        discordChannels: (payload?.discordChannels ?? []).filter(isDiscordChannelAccessRecord).map((channel) => ({
            ...channel,
            artifactDelivery: normalizeArtifactDeliveryMode(channel.artifactDelivery),
            allowedGroupIds: channel.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
        })),
        slackIdentities: (payload?.slackIdentities ?? []).filter((item) => isSlackIdentityRecord(item) && userIds.has(item.userId)),
        slackChannels: (payload?.slackChannels ?? []).filter(isSlackChannelAccessRecord).map((channel) => ({
            ...channel,
            artifactDelivery: normalizeArtifactDeliveryMode(channel.artifactDelivery),
            allowedGroupIds: channel.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
        })),
        matrixIdentities: (payload?.matrixIdentities ?? []).filter((item) => isMatrixIdentityRecord(item) && userIds.has(item.userId)),
        matrixRooms: (payload?.matrixRooms ?? []).filter(isMatrixRoomAccessRecord).map((room) => ({
            ...room,
            artifactDelivery: normalizeArtifactDeliveryMode(room.artifactDelivery),
            allowedGroupIds: room.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
        })),
        totpCredentials: (payload?.totpCredentials ?? []).filter((item) => isTotpCredentialRecord(item) && userIds.has(item.userId)),
        recoveryCodes: (payload?.recoveryCodes ?? []).filter((item) => isRecoveryCodeRecord(item) && userIds.has(item.userId)),
        webAuthnCredentials: (payload?.webAuthnCredentials ?? []).filter((item) => isWebAuthnCredentialRecord(item) && userIds.has(item.userId)),
        apiTokens: (payload?.apiTokens ?? []).filter((item) => isApiTokenRecord(item) && userIds.has(item.userId)).map((token) => ({
            ...token,
            permissions: normalizePermissions(token.permissions),
            agentIds: normalizeStringList(token.agentIds),
            workspaceRoots: normalizeStringList(token.workspaceRoots),
            peerIds: normalizeStringList(token.peerIds),
        })),
        webSessions: (payload?.webSessions ?? []).filter((item) => isWebSessionRecord(item) && userIds.has(item.userId)),
        telegramLinkCodes: (payload?.telegramLinkCodes ?? []).filter((item) => isTelegramLinkCodeRecord(item) && userIds.has(item.userId)),
        discordLinkCodes: (payload?.discordLinkCodes ?? []).filter((item) => isDiscordLinkCodeRecord(item) && userIds.has(item.userId)),
        slackLinkCodes: (payload?.slackLinkCodes ?? []).filter((item) => isSlackLinkCodeRecord(item) && userIds.has(item.userId)),
        matrixLinkCodes: (payload?.matrixLinkCodes ?? []).filter((item) => isMatrixLinkCodeRecord(item) && userIds.has(item.userId)),
    };
}
export function normalizeEmail(email) {
    return email.trim().toLowerCase();
}
export function normalizeGroupIds(payload, values, emptyFallback = READONLY_GROUP_ID) {
    const available = new Set(payload.groups.map((group) => group.id));
    const groupIds = Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
    for (const groupId of groupIds) {
        if (!available.has(groupId)) {
            throw new Error(`Unknown group: ${groupId}`);
        }
    }
    return groupIds.length > 0 ? groupIds : (emptyFallback ? [emptyFallback] : []);
}
export function normalizePermissions(values, strict = false) {
    const permissions = [];
    for (const value of values ?? []) {
        if (isPermission(value)) {
            if (!permissions.includes(value)) {
                permissions.push(value);
            }
            continue;
        }
        if (strict && value.trim()) {
            throw new Error(`Unknown permission: ${value}`);
        }
    }
    return permissions;
}
export function normalizeStringList(values) {
    return Array.from(new Set((values ?? []).map((value) => value.trim()).filter(Boolean)));
}
export function normalizeNumberList(values) {
    return Array.from(new Set((values ?? []).filter((value) => Number.isInteger(value))));
}
export function normalizeDiscordId(value) {
    const normalized = String(value ?? "").trim();
    return normalized || undefined;
}
export function normalizeSlackId(value) {
    const normalized = String(value ?? "").trim();
    return normalized || undefined;
}
export function normalizeMatrixId(value) {
    const normalized = String(value ?? "").trim();
    return normalized || undefined;
}
export function assertActiveAdminExists(payload) {
    const hasAdmin = payload.users.some((user) => user.active && payload.userGroups.some((item) => item.userId === user.id && item.groupId === ADMIN_GROUP_ID));
    if (!hasAdmin) {
        throw new Error("Cannot remove or disable the last active admin user.");
    }
}
export function normalizeWorkspacePath(value) {
    return path.resolve(value);
}
export function isPathInside(candidate, root) {
    const relative = path.relative(root, candidate);
    return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
export function slugify(value) {
    return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
export function allPermissionsSafe() {
    return [...BUILTIN_GROUPS.find((group) => group.id === ADMIN_GROUP_ID).permissions];
}
export function normalizeUserPreferences(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
        return undefined;
    }
    const candidate = value;
    const theme = candidate.theme === "light" || candidate.theme === "dark" || candidate.theme === "system"
        ? candidate.theme
        : undefined;
    const artifactDelivery = normalizeArtifactDeliveryMode(candidate.artifactDelivery);
    return theme || artifactDelivery ? { theme, artifactDelivery } : undefined;
}
export function normalizeArtifactDeliveryMode(value) {
    return isArtifactDeliveryMode(value) ? value : undefined;
}
function isUserRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.email === "string" &&
        typeof candidate.displayName === "string" && typeof candidate.passwordHash === "string" &&
        typeof candidate.passwordSalt === "string" && typeof candidate.active === "boolean";
}
function isGroupRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.name === "string" &&
        Array.isArray(candidate.permissions);
}
function isUserGroupRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.userId === "string" && typeof candidate.groupId === "string";
}
function isTelegramIdentityRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
        Number.isInteger(candidate.telegramUserId) && typeof candidate.active === "boolean";
}
function isTelegramChatAccessRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && Number.isInteger(candidate.chatId) &&
        typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
}
function isDiscordIdentityRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
        typeof candidate.discordUserId === "string" && typeof candidate.active === "boolean";
}
function isDiscordChannelAccessRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.channelId === "string" &&
        typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
}
function isSlackIdentityRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
        typeof candidate.slackUserId === "string" && typeof candidate.active === "boolean";
}
function isSlackChannelAccessRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.channelId === "string" &&
        typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
}
function isMatrixIdentityRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
        typeof candidate.matrixUserId === "string" && typeof candidate.active === "boolean";
}
function isMatrixRoomAccessRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.roomId === "string" &&
        typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
}
function isTotpCredentialRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.userId === "string" && typeof candidate.secret === "string" &&
        typeof candidate.enabledAt === "string";
}
function isRecoveryCodeRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
        typeof candidate.codeHash === "string" && typeof candidate.createdAt === "string";
}
function isWebAuthnCredentialRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
        typeof candidate.credentialId === "string" && typeof candidate.publicKey === "string" &&
        typeof candidate.counter === "number" && typeof candidate.name === "string";
}
function isApiTokenRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
        typeof candidate.name === "string" && typeof candidate.tokenHash === "string" &&
        typeof candidate.tokenPrefix === "string" && Array.isArray(candidate.permissions) &&
        typeof candidate.createdAt === "string";
}
function isWebSessionRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
        typeof candidate.tokenHash === "string" && typeof candidate.expiresAt === "string";
}
function isTelegramLinkCodeRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
        typeof candidate.expiresAt === "string";
}
function isDiscordLinkCodeRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
        typeof candidate.expiresAt === "string";
}
function isSlackLinkCodeRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
        typeof candidate.expiresAt === "string";
}
function isMatrixLinkCodeRecord(value) {
    const candidate = value;
    return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
        typeof candidate.expiresAt === "string";
}
