import { createHash, createHmac, randomBytes } from "node:crypto"
import { existsSync, readFileSync } from "node:fs"
import { resolve } from "node:path"
import { FastifyInstance } from "fastify"
import { authProfiles, authTenantUsers, authUsers, communicationRooms, tenants } from "../../db/schema"
import { and, eq, isNotNull } from "drizzle-orm"
import { secrets } from "../utils/secrets"
import jwt from "jsonwebtoken"

type MatrixErrorResponse = {
    errcode?: string
    error?: string
    retry_after_ms?: number
}

type MatrixRoomEvent = {
    event_id: string
    sender: string
    origin_server_ts: number
    type: string
    redacts?: string
    content?: {
        body?: string
        msgtype?: string
        url?: string
        info?: {
            mimetype?: string
            size?: number
        }
        "m.new_content"?: {
            body?: string
            msgtype?: string
        }
        "m.relates_to"?: {
            event_id?: string
            key?: string
            rel_type?: string
            "m.in_reply_to"?: {
                event_id?: string
            }
        }
    }
}

type MatrixJoinedMembersResponse = {
    joined: Record<string, {
        display_name?: string
        avatar_url?: string
    }>
}

type MatrixRoomSearchResponse = {
    search_categories?: {
        room_events?: {
            count?: number
            results?: Array<{
                result?: MatrixRoomEvent & {
                    room_id?: string
                }
            }>
        }
    }
}

type MatrixSyncResponse = {
    next_batch?: string
    rooms?: {
        join?: Record<string, {
            timeline?: {
                events?: MatrixRoomEvent[]
            }
            state?: {
                events?: MatrixRoomEvent[]
            }
        }>
    }
}

type MatrixUserSession = {
    accessToken: string
    matrixUserId: string
    validUntilMs: number
}

type MatrixLoginTokenResponse = {
    login_token: string
    expires_in_ms: number
}

type LiveKitGrant = {
    roomJoin: boolean
    room: string
    canPublish: boolean
    canSubscribe: boolean
}

type MatrixTenantRoomOptions = {
    key?: string
    name?: string
    topic?: string
    type?: string
    entityType?: string | null
    entityId?: number | null
    entityUuid?: string | null
    inviteUserIds?: string[]
}

type MatrixAttachmentInput = {
    buffer: Buffer
    filename: string
    mimeType: string
    size: number
}

type MatrixMessageOptions = {
    replyToEventId?: string
}

type MatrixCachedValue<T = any> = {
    exists: true
    cachedUntil: number
    value: T
}

const matrixUserSessionCache = new Map<string, MatrixUserSession>()
const matrixJoinedRoomCache = new Map<string, number>()
const matrixProvisionedUserCache = new Map<string, number>()
const matrixTenantSpaceCache = new Map<string, MatrixCachedValue>()
const matrixTenantRoomCache = new Map<string, MatrixCachedValue>()
let matrixServiceSessionCache: MatrixUserSession | null = null

const defaultTenantRooms: Required<Pick<MatrixTenantRoomOptions, "key" | "name" | "type">>[] = [
    {
        key: "allgemein",
        name: "Allgemeiner Chat",
        type: "general",
    },
]

const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "")
const readLocalDevRegistrationSharedSecret = () => {
    if (process.env.NODE_ENV === "production") return ""

    const candidates = [
        resolve(process.cwd(), "../matrix/dev/synapse/homeserver.yaml"),
        resolve(process.cwd(), "matrix/dev/synapse/homeserver.yaml"),
    ]

    for (const candidate of candidates) {
        if (!existsSync(candidate)) continue

        const content = readFileSync(candidate, "utf8")
        const match = content.match(/^registration_shared_secret:\s*["']?(.+?)["']?\s*$/m)

        if (match?.[1]) {
            return match[1]
        }
    }

    return ""
}

const normalizeMatrixLocalpartSeed = (value: string) => {
    const normalized = value
        .toLowerCase()
        .normalize("NFKD")
        .replace(/[\u0300-\u036f]/g, "")
        .replace(/ä/g, "a")
        .replace(/ö/g, "o")
        .replace(/ü/g, "u")
        .replace(/ß/g, "ss")
        .replace(/[^a-z0-9._=-]+/g, "_")
        .replace(/_+/g, "_")
        .replace(/^[._=-]+|[._=-]+$/g, "")

    return normalized || "user"
}

const normalizeMatrixAliasSeed = (value: string) =>
    normalizeMatrixLocalpartSeed(value)
        .replace(/[.=]/g, "_")
        .replace(/_+/g, "_")

export function matrixService(server: FastifyInstance) {
    const homeserverUrl = () =>
        trimTrailingSlash(
            process.env.MATRIX_HOMESERVER_URL ||
            secrets.MATRIX_HOMESERVER_URL ||
            "http://localhost:8008"
        )

    const serverName = () =>
        process.env.MATRIX_SERVER_NAME ||
        secrets.MATRIX_SERVER_NAME ||
        "localhost"

    const registrationSharedSecret = () =>
        process.env.MATRIX_REGISTRATION_SHARED_SECRET ||
        secrets.MATRIX_REGISTRATION_SHARED_SECRET ||
        readLocalDevRegistrationSharedSecret() ||
        ""

    const rtcHost = () =>
        process.env.MATRIX_RTC_HOST ||
        secrets.MATRIX_RTC_HOST ||
        "call.fedeo.de"

    const rtcJwtUrl = () =>
        process.env.MATRIX_RTC_JWT_URL ||
        secrets.MATRIX_RTC_JWT_URL ||
        (process.env.NODE_ENV === "production"
            ? `https://${rtcHost()}/livekit/jwt`
            : `http://localhost:${process.env.MATRIX_DEV_RTC_JWT_PORT || "8081"}`)

    const livekitUrl = () =>
        process.env.MATRIX_LIVEKIT_URL ||
        secrets.MATRIX_LIVEKIT_URL ||
        (process.env.NODE_ENV === "production"
            ? `wss://${rtcHost()}/livekit/sfu`
            : `ws://localhost:${process.env.MATRIX_DEV_LIVEKIT_PORT || "7880"}`)

    const livekitKey = () =>
        process.env.LIVEKIT_KEY ||
        secrets.LIVEKIT_KEY ||
        (process.env.NODE_ENV === "production" ? "" : "devkey")

    const livekitSecret = () =>
        process.env.LIVEKIT_SECRET ||
        secrets.LIVEKIT_SECRET ||
        (process.env.NODE_ENV === "production" ? "" : "devsecret-local-matrix-stack-32-chars")

    const serviceUserLocalpart = () =>
        process.env.MATRIX_SERVICE_USER_LOCALPART ||
        secrets.MATRIX_SERVICE_USER_LOCALPART ||
        "fedeo_service"

    const serviceUserPassword = () =>
        createHmac("sha256", registrationSharedSecret())
            .update(`${serverName()}:fedeo-service-user`)
            .digest("base64url")

    const getUserIdentitySeed = async (userId: string, tenantId: number | null) => {
        const [user] = await server.db
            .select({ email: authUsers.email })
            .from(authUsers)
            .where(eq(authUsers.id, userId))
            .limit(1)

        if (user?.email) {
            return user.email.split("@")[0] || user.email
        }

        if (tenantId) {
            const displayName = await getCurrentUserDisplayName(userId, tenantId)
            if (displayName && !displayName.startsWith("@")) {
                return displayName
            }
        }

        return "user"
    }

    const matrixLocalpartForUser = async (userId: string, tenantId: number | null) => {
        const seed = normalizeMatrixLocalpartSeed(await getUserIdentitySeed(userId, tenantId))
        const hash = createHash("sha256").update(userId).digest("hex").slice(0, 8)
        return `${seed}_${hash}`
    }

    const matrixUserIdForUser = async (userId: string, tenantId: number | null) =>
        `@${await matrixLocalpartForUser(userId, tenantId)}:${serverName()}`

    const tenantSpaceAliasLocalpart = (tenant: { id: number, short?: string | null, name?: string | null }) => {
        const seed = normalizeMatrixAliasSeed(tenant.short || tenant.name || `tenant_${tenant.id}`)
        return `fedeo_${seed}_${tenant.id}`
    }

    const tenantSpaceAlias = (tenant: { id: number, short?: string | null, name?: string | null }) =>
        `#${tenantSpaceAliasLocalpart(tenant)}:${serverName()}`

    const tenantRoomAliasLocalpart = (
        tenant: { id: number, short?: string | null, name?: string | null },
        roomKey: string
    ) => {
        const tenantSeed = normalizeMatrixAliasSeed(tenant.short || tenant.name || `tenant_${tenant.id}`)
        const roomSeed = normalizeMatrixAliasSeed(roomKey)
        return `fedeo_${tenantSeed}_${tenant.id}_${roomSeed}`
    }

    const tenantRoomAlias = (
        tenant: { id: number, short?: string | null, name?: string | null },
        roomKey: string
    ) => `#${tenantRoomAliasLocalpart(tenant, roomKey)}:${serverName()}`

    const matrixIdentifierServerName = (value?: string | null) => {
        const match = value?.match(/^[!#@][^:]+:(.+)$/)
        return match?.[1] || null
    }

    const belongsToCurrentMatrixServer = (value?: string | null) => {
        const identifierServerName = matrixIdentifierServerName(value)
        return !identifierServerName || identifierServerName === serverName()
    }

    const normalizeTenantRoomOptions = (options: MatrixTenantRoomOptions = {}) => {
        const fallbackName = options.key || options.name || "Allgemeiner Chat"
        const key = normalizeMatrixAliasSeed(options.key || fallbackName)
        const name = (options.name || fallbackName).trim() || "Allgemeiner Chat"
        const topic = options.topic?.trim()

        return {
            key,
            name,
            topic,
            type: options.type || "room",
            entityType: options.entityType || null,
            entityId: options.entityId || null,
            entityUuid: options.entityUuid || null,
            inviteUserIds: options.inviteUserIds || [],
        }
    }

    const buildSharedSecretMac = (
        nonce: string,
        username: string,
        password: string,
        admin: boolean
    ) => {
        const hmac = createHmac("sha1", registrationSharedSecret())
        hmac.update(nonce)
        hmac.update("\0")
        hmac.update(username)
        hmac.update("\0")
        hmac.update(password)
        hmac.update("\0")
        hmac.update(admin ? "admin" : "notadmin")
        return hmac.digest("hex")
    }

    const registerWithSharedSecret = async (
        username: string,
        password: string,
        admin: boolean
    ) => {
        const nonceResponse = await requestJson<{ nonce: string }>(
            `${homeserverUrl()}/_synapse/admin/v1/register`
        )
        const mac = buildSharedSecretMac(nonceResponse.nonce, username, password, admin)

        return requestJson(`${homeserverUrl()}/_synapse/admin/v1/register`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
                nonce: nonceResponse.nonce,
                username,
                password,
                admin,
                mac,
            }),
        })
    }

    const loginMatrixUser = async (username: string, password: string) => {
        return requestJson<{
            access_token: string
            user_id: string
        }>(`${homeserverUrl()}/_matrix/client/v3/login`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
                type: "m.login.password",
                identifier: {
                    type: "m.id.user",
                    user: username,
                },
                password,
            }),
        })
    }

    const ensureServiceAccessToken = async () => {
        if (!registrationSharedSecret()) {
            throw Object.assign(
                new Error("MATRIX_REGISTRATION_SHARED_SECRET is not configured"),
                { statusCode: 503 }
            )
        }

        if (matrixServiceSessionCache && matrixServiceSessionCache.validUntilMs > Date.now() + 60_000) {
            return matrixServiceSessionCache
        }

        const username = serviceUserLocalpart()
        const password = serviceUserPassword()

        try {
            const login = await loginMatrixUser(username, password)
            matrixServiceSessionCache = {
                accessToken: login.access_token,
                matrixUserId: login.user_id,
                validUntilMs: Date.now() + 30 * 60 * 1000,
            }

            return matrixServiceSessionCache
        } catch (loginErr: any) {
            if (loginErr.statusCode === 429) throw loginErr
        }

        try {
            await registerWithSharedSecret(username, password, true)
        } catch (registerErr: any) {
            if (registerErr.errcode !== "M_USER_IN_USE") throw registerErr
        }
        const login = await loginMatrixUser(username, password)
        matrixServiceSessionCache = {
            accessToken: login.access_token,
            matrixUserId: login.user_id,
            validUntilMs: Date.now() + 30 * 60 * 1000,
        }

        return matrixServiceSessionCache
    }

    const getCurrentUserDisplayName = async (userId: string, tenantId: number | null) => {
        if (tenantId) {
            const [profile] = await server.db
                .select({
                    firstName: authProfiles.first_name,
                    lastName: authProfiles.last_name,
                })
                .from(authProfiles)
                .where(and(
                    eq(authProfiles.user_id, userId),
                    eq(authProfiles.tenant_id, tenantId)
                ))
                .limit(1)

            const profileName = [profile?.firstName, profile?.lastName]
                .filter(Boolean)
                .join(" ")
                .trim()

            if (profileName) return profileName
        }

        const [user] = await server.db
            .select({ email: authUsers.email })
            .from(authUsers)
            .where(eq(authUsers.id, userId))
            .limit(1)

        return user?.email || await matrixUserIdForUser(userId, tenantId)
    }

    const requestJson = async <T>(url: string, init?: RequestInit): Promise<T> => {
        const response = await fetch(url, init)
        const text = await response.text()
        const body = text ? JSON.parse(text) : {}

        if (!response.ok) {
            const error = body as MatrixErrorResponse
            throw Object.assign(
                new Error(error.error || `Matrix request failed with ${response.status}`),
                {
                    statusCode: response.status,
                    errcode: error.errcode,
                    retryAfterMs: error.retry_after_ms,
                    body,
                }
            )
        }

        return body as T
    }

    const requestMatrixJson = async <T>(path: string, accessToken: string, init?: RequestInit): Promise<T> => {
        return requestJson<T>(`${homeserverUrl()}${path}`, {
            ...init,
            headers: {
                ...(init?.headers || {}),
                Authorization: `Bearer ${accessToken}`,
            },
        })
    }

    const mxcToMediaPath = (mxcUri: string) => {
        const match = mxcUri.match(/^mxc:\/\/([^/]+)\/(.+)$/)
        if (!match) {
            throw Object.assign(
                new Error("Ungültige Matrix-Media-URI"),
                { statusCode: 400 }
            )
        }

        return `/_matrix/media/v3/download/${encodeURIComponent(match[1])}/${encodeURIComponent(match[2])}`
    }

    const matrixMediaUrl = (mxcUri: string) => `${homeserverUrl()}${mxcToMediaPath(mxcUri)}`

    const attachmentFromEvent = (event: MatrixRoomEvent) => {
        const msgtype = event.content?.msgtype || "m.text"

        if (!["m.file", "m.image"].includes(msgtype) || !event.content?.url) {
            return null
        }

        const mimeType = event.content.info?.mimetype || "application/octet-stream"

        return {
            type: msgtype === "m.image" ? "image" : "file",
            url: event.content.url,
            fileName: event.content.body || "Anhang",
            mimeType,
            size: event.content.info?.size || 0,
            previewUrl: msgtype === "m.image" ? matrixMediaUrl(event.content.url) : null,
            downloadUrl: matrixMediaUrl(event.content.url),
            isImage: msgtype === "m.image" || mimeType.startsWith("image/"),
        }
    }

    const createAccessTokenForUser = async (userId: string, tenantId: number | null) => {
        const matrixUserId = await matrixUserIdForUser(userId, tenantId)
        const cacheKey = `${tenantId || "global"}:${userId}`
        const cachedSession = matrixUserSessionCache.get(cacheKey)

        if (cachedSession && cachedSession.validUntilMs > Date.now() + 60_000) {
            return cachedSession
        }

        await provisionCurrentUser(userId, tenantId)

        const serviceLogin = await ensureServiceAccessToken()
        const validUntilMs = Date.now() + 30 * 60 * 1000

        const login = await requestMatrixJson<{ access_token: string }>(
            `/_synapse/admin/v1/users/${encodeURIComponent(matrixUserId)}/login`,
            serviceLogin.accessToken,
            {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ valid_until_ms: validUntilMs }),
            }
        )

        const session = {
            accessToken: login.access_token,
            matrixUserId,
            validUntilMs,
        }

        matrixUserSessionCache.set(cacheKey, session)
        return session
    }

    const getStatus = async () => {
        const configured = Boolean(homeserverUrl() && serverName())

        if (!configured) {
            return {
                configured: false,
                homeserverUrl: homeserverUrl(),
                serverName: serverName(),
                provisioningConfigured: Boolean(registrationSharedSecret()),
                reachable: false,
                calls: {
                    provider: "matrixrtc-livekit",
                    configured: Boolean(rtcJwtUrl() && livekitUrl()),
                    rtcHost: rtcHost(),
                    rtcJwtUrl: rtcJwtUrl(),
                    livekitUrl: livekitUrl(),
                },
            }
        }

        try {
            const versions = await requestJson<{ versions: string[] }>(
                `${homeserverUrl()}/_matrix/client/versions`
            )

            return {
                configured: true,
                homeserverUrl: homeserverUrl(),
                serverName: serverName(),
                provisioningConfigured: Boolean(registrationSharedSecret()),
                reachable: true,
                calls: {
                    provider: "matrixrtc-livekit",
                    configured: Boolean(rtcJwtUrl() && livekitUrl()),
                    rtcHost: rtcHost(),
                    rtcJwtUrl: rtcJwtUrl(),
                    livekitUrl: livekitUrl(),
                },
                versions: versions.versions,
            }
        } catch (err: any) {
            return {
                configured: true,
                homeserverUrl: homeserverUrl(),
                serverName: serverName(),
                provisioningConfigured: Boolean(registrationSharedSecret()),
                reachable: false,
                calls: {
                    provider: "matrixrtc-livekit",
                    configured: Boolean(rtcJwtUrl() && livekitUrl()),
                    rtcHost: rtcHost(),
                    rtcJwtUrl: rtcJwtUrl(),
                    livekitUrl: livekitUrl(),
                },
                error: err.message,
            }
        }
    }

    const provisionCurrentUser = async (userId: string, tenantId: number | null) => {
        if (!registrationSharedSecret()) {
            throw Object.assign(
                new Error("MATRIX_REGISTRATION_SHARED_SECRET is not configured"),
                { statusCode: 503 }
            )
        }

        const username = await matrixLocalpartForUser(userId, tenantId)
        const matrixUserId = await matrixUserIdForUser(userId, tenantId)
        const displayName = await getCurrentUserDisplayName(userId, tenantId)
        const cacheKey = `${tenantId || "global"}:${userId}`
        const cachedUntil = matrixProvisionedUserCache.get(cacheKey)

        if (cachedUntil && cachedUntil > Date.now()) {
            return {
                matrixUserId,
                localpart: username,
                displayName,
                created: false,
                alreadyExisted: true,
            }
        }

        const password = randomBytes(32).toString("base64url")

        try {
            await registerWithSharedSecret(username, password, false)
            matrixProvisionedUserCache.set(cacheKey, Date.now() + 30 * 60 * 1000)

            return {
                matrixUserId,
                localpart: username,
                displayName,
                created: true,
                alreadyExisted: false,
            }
        } catch (err: any) {
            if (err.errcode === "M_USER_IN_USE") {
                matrixProvisionedUserCache.set(cacheKey, Date.now() + 30 * 60 * 1000)

                return {
                    matrixUserId,
                    localpart: username,
                    displayName,
                    created: false,
                    alreadyExisted: true,
                }
            }

            throw err
        }
    }

    const getCurrentTenant = async (tenantId: number | null) => {
        if (!tenantId) {
            throw Object.assign(
                new Error("No active tenant selected"),
                { statusCode: 400 }
            )
        }

        const [tenant] = await server.db
            .select({
                id: tenants.id,
                name: tenants.name,
                short: tenants.short,
            })
            .from(tenants)
            .where(eq(tenants.id, tenantId))
            .limit(1)

        if (!tenant) {
            throw Object.assign(
                new Error("Tenant not found"),
                { statusCode: 404 }
            )
        }

        return tenant
    }

    const getTenantSpaceStatus = async (tenantId: number | null) => {
        const tenant = await getCurrentTenant(tenantId)
        const alias = tenantSpaceAlias(tenant)

        try {
            const directoryEntry = await requestJson<{
                room_id: string
                servers: string[]
            }>(`${homeserverUrl()}/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`)

            return {
                tenantId: tenant.id,
                tenantName: tenant.name,
                alias,
                exists: true,
                roomId: directoryEntry.room_id,
                servers: directoryEntry.servers,
            }
        } catch (err: any) {
            if (err.statusCode === 404 || err.errcode === "M_NOT_FOUND") {
                return {
                    tenantId: tenant.id,
                    tenantName: tenant.name,
                    alias,
                    exists: false,
                    roomId: null,
                    servers: [],
                }
            }

            throw err
        }
    }

    const provisionCurrentTenantSpace = async (userId: string, tenantId: number | null) => {
        const tenant = await getCurrentTenant(tenantId)
        const cacheKey = String(tenant.id)
        const cachedSpace = matrixTenantSpaceCache.get(cacheKey)

        if (cachedSpace?.exists && cachedSpace.cachedUntil > Date.now()) {
            return cachedSpace.value
        }

        const existing = await getTenantSpaceStatus(tenant.id)
        const userAccount = await provisionCurrentUser(userId, tenant.id)

        if (existing.exists) {
            const value = {
                ...existing,
                created: false,
                alreadyExisted: true,
                invitedUserId: userAccount.matrixUserId,
            }

            matrixTenantSpaceCache.set(cacheKey, {
                exists: true,
                cachedUntil: Date.now() + 30 * 60 * 1000,
                value,
            })
            return value
        }

        const serviceLogin = await ensureServiceAccessToken()
        const aliasLocalpart = tenantSpaceAliasLocalpart(tenant)
        const createdRoom = await requestMatrixJson<{ room_id: string }>(
            "/_matrix/client/v3/createRoom",
            serviceLogin.accessToken,
            {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    creation_content: {
                        type: "m.space",
                    },
                    name: `FEDEO · ${tenant.name}`,
                    topic: `Kommunikationsbereich für ${tenant.name}`,
                    preset: "private_chat",
                    visibility: "private",
                    room_alias_name: aliasLocalpart,
                    invite: [userAccount.matrixUserId],
                    initial_state: [
                        {
                            type: "m.room.history_visibility",
                            state_key: "",
                            content: {
                                history_visibility: "invited",
                            },
                        },
                    ],
                }),
            }
        )

        const value = {
            tenantId: tenant.id,
            tenantName: tenant.name,
            alias: tenantSpaceAlias(tenant),
            exists: true,
            created: true,
            alreadyExisted: false,
            roomId: createdRoom.room_id,
            invitedUserId: userAccount.matrixUserId,
            serviceUserId: serviceLogin.matrixUserId,
        }

        matrixTenantSpaceCache.set(cacheKey, {
            exists: true,
            cachedUntil: Date.now() + 30 * 60 * 1000,
            value,
        })
        return value
    }

    const roomMetadataToApi = (
        tenant: { id: number, name?: string | null, short?: string | null },
        room: typeof communicationRooms.$inferSelect
    ) => ({
        id: room.id,
        tenantId: tenant.id,
        tenantName: tenant.name,
        key: room.key,
        name: room.name,
        topic: room.topic,
        type: room.type,
        entityType: room.entityType,
        entityId: room.entityId,
        entityUuid: room.entityUuid,
        alias: room.matrixAlias || tenantRoomAlias(tenant, room.key),
        exists: Boolean(room.matrixRoomId),
        roomId: room.matrixRoomId,
        parentSpaceRoomId: room.parentSpaceRoomId,
        servers: [],
        archived: room.archived,
    })

    const findTenantRoomMetadata = async (tenantId: number, key: string) => {
        const [room] = await server.db
            .select()
            .from(communicationRooms)
            .where(and(
                eq(communicationRooms.tenantId, tenantId),
                eq(communicationRooms.key, key)
            ))
            .limit(1)

        return room
    }

    const ensureTenantRoomMetadata = async (
        tenant: { id: number, name?: string | null, short?: string | null },
        options: MatrixTenantRoomOptions
    ) => {
        const normalizedOptions = normalizeTenantRoomOptions(options)
        const existing = await findTenantRoomMetadata(tenant.id, normalizedOptions.key)
        const expectedAlias = tenantRoomAlias(tenant, normalizedOptions.key)

        if (existing) {
            const hasStaleMatrixRoomId = !belongsToCurrentMatrixServer(existing.matrixRoomId)
            const hasStaleMatrixAlias = !belongsToCurrentMatrixServer(existing.matrixAlias)
            const shouldUpdate =
                hasStaleMatrixRoomId ||
                hasStaleMatrixAlias ||
                (options.name !== undefined && existing.name !== normalizedOptions.name) ||
                (options.topic !== undefined && existing.topic !== normalizedOptions.topic) ||
                (options.type !== undefined && existing.type !== normalizedOptions.type) ||
                (options.entityType !== undefined && existing.entityType !== normalizedOptions.entityType) ||
                (options.entityId !== undefined && existing.entityId !== normalizedOptions.entityId) ||
                (options.entityUuid !== undefined && existing.entityUuid !== normalizedOptions.entityUuid)

            if (!shouldUpdate) return existing

            const [updated] = await server.db
                .update(communicationRooms)
                .set({
                    name: options.name !== undefined ? normalizedOptions.name : existing.name,
                    topic: options.topic !== undefined ? normalizedOptions.topic : existing.topic,
                    type: options.type !== undefined ? normalizedOptions.type : existing.type,
                    entityType: options.entityType !== undefined ? normalizedOptions.entityType : existing.entityType,
                    entityId: options.entityId !== undefined ? normalizedOptions.entityId : existing.entityId,
                    entityUuid: options.entityUuid !== undefined ? normalizedOptions.entityUuid : existing.entityUuid,
                    matrixRoomId: hasStaleMatrixRoomId ? null : existing.matrixRoomId,
                    matrixAlias: hasStaleMatrixAlias ? expectedAlias : existing.matrixAlias,
                    parentSpaceRoomId: hasStaleMatrixRoomId ? null : existing.parentSpaceRoomId,
                    updatedAt: new Date(),
                })
                .where(eq(communicationRooms.id, existing.id))
                .returning()

            return updated
        }

        const [created] = await server.db
            .insert(communicationRooms)
            .values({
                tenantId: tenant.id,
                key: normalizedOptions.key,
                name: normalizedOptions.name,
                topic: normalizedOptions.topic,
                type: normalizedOptions.type,
                entityType: normalizedOptions.entityType,
                entityId: normalizedOptions.entityId,
                entityUuid: normalizedOptions.entityUuid,
                matrixAlias: expectedAlias,
            })
            .returning()

        return created
    }

    const markTenantRoomProvisioned = async (
        metadataId: string,
        values: {
            matrixRoomId: string
            matrixAlias: string
            parentSpaceRoomId?: string | null
        }
    ) => {
        const [updated] = await server.db
            .update(communicationRooms)
            .set({
                matrixRoomId: values.matrixRoomId,
                matrixAlias: values.matrixAlias,
                parentSpaceRoomId: values.parentSpaceRoomId || null,
                updatedAt: new Date(),
            })
            .where(eq(communicationRooms.id, metadataId))
            .returning()

        return updated
    }

    const getTenantRoomStatus = async (
        tenantId: number | null,
        roomKey: string,
        roomName?: string
    ) => {
        const tenant = await getCurrentTenant(tenantId)
        const normalizedOptions = normalizeTenantRoomOptions({ key: roomKey, name: roomName })
        const metadata = await ensureTenantRoomMetadata(tenant, normalizedOptions)
        const alias = metadata.matrixAlias || tenantRoomAlias(tenant, normalizedOptions.key)

        try {
            const directoryEntry = await requestJson<{
                room_id: string
                servers: string[]
            }>(`${homeserverUrl()}/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`)
            const roomMetadata = metadata.matrixRoomId === directoryEntry.room_id
                ? metadata
                : await markTenantRoomProvisioned(metadata.id, {
                    matrixRoomId: directoryEntry.room_id,
                    matrixAlias: alias,
                    parentSpaceRoomId: metadata.parentSpaceRoomId,
                })

            return {
                tenantId: tenant.id,
                tenantName: tenant.name,
                id: roomMetadata.id,
                key: roomMetadata.key,
                name: roomMetadata.name,
                topic: roomMetadata.topic,
                type: roomMetadata.type,
                entityType: roomMetadata.entityType,
                entityId: roomMetadata.entityId,
                entityUuid: roomMetadata.entityUuid,
                alias,
                exists: true,
                roomId: directoryEntry.room_id,
                parentSpaceRoomId: roomMetadata.parentSpaceRoomId,
                servers: directoryEntry.servers,
            }
        } catch (err: any) {
            if (err.statusCode === 404 || err.errcode === "M_NOT_FOUND") {
                return {
                    tenantId: tenant.id,
                    tenantName: tenant.name,
                    id: metadata.id,
                    key: metadata.key,
                    name: metadata.name,
                    topic: metadata.topic,
                    type: metadata.type,
                    entityType: metadata.entityType,
                    entityId: metadata.entityId,
                    entityUuid: metadata.entityUuid,
                    alias,
                    exists: false,
                    roomId: belongsToCurrentMatrixServer(metadata.matrixRoomId) ? metadata.matrixRoomId : null,
                    parentSpaceRoomId: belongsToCurrentMatrixServer(metadata.parentSpaceRoomId) ? metadata.parentSpaceRoomId : null,
                    servers: [],
                }
            }

            throw err
        }
    }

    const provisionTenantRoom = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {}
    ) => {
        const tenant = await getCurrentTenant(tenantId)
        const normalizedOptions = normalizeTenantRoomOptions(options)
        const metadata = await ensureTenantRoomMetadata(tenant, normalizedOptions)
        const key = normalizedOptions.key
        const name = normalizedOptions.name
        const topic = (normalizedOptions.topic || `Allgemeiner Kommunikationsraum für ${tenant.name}`).trim()
        const cacheKey = `${tenant.id}:${key}`
        const cachedRoom = matrixTenantRoomCache.get(cacheKey)

        if (cachedRoom?.exists && cachedRoom.cachedUntil > Date.now()) {
            return cachedRoom.value
        }

        const existing = await getTenantRoomStatus(tenant.id, key, name)
        const userAccount = await provisionCurrentUser(userId, tenant.id)
        const invitedMatrixUserIds = await matrixUserIdsForInvitees(userId, tenant.id, normalizedOptions.inviteUserIds || [])
        const tenantSpace = await provisionCurrentTenantSpace(userId, tenant.id)

        if (existing.exists) {
            await markTenantRoomProvisioned(metadata.id, {
                matrixRoomId: existing.roomId,
                matrixAlias: existing.alias,
                parentSpaceRoomId: existing.parentSpaceRoomId,
            })

            const value = {
                ...existing,
                created: false,
                alreadyExisted: true,
                parentSpaceRoomId: tenantSpace.roomId,
                invitedUserId: userAccount.matrixUserId,
            }

            await inviteUsersToRoom(existing.roomId, invitedMatrixUserIds)

            matrixTenantRoomCache.set(cacheKey, {
                exists: true,
                cachedUntil: Date.now() + 30 * 60 * 1000,
                value,
            })
            return value
        }

        const serviceLogin = await ensureServiceAccessToken()
        const createdRoom = await requestMatrixJson<{ room_id: string }>(
            "/_matrix/client/v3/createRoom",
            serviceLogin.accessToken,
            {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    name,
                    topic,
                    preset: "private_chat",
                    visibility: "private",
                    room_alias_name: tenantRoomAliasLocalpart(tenant, key),
                    invite: Array.from(new Set([userAccount.matrixUserId, ...invitedMatrixUserIds])),
                    initial_state: [
                        {
                            type: "m.room.history_visibility",
                            state_key: "",
                            content: {
                                history_visibility: "invited",
                            },
                        },
                        {
                            type: "m.space.parent",
                            state_key: tenantSpace.roomId,
                            content: {
                                via: [serverName()],
                                canonical: true,
                            },
                        },
                    ],
                }),
            }
        )

        await requestMatrixJson(
            `/_matrix/client/v3/rooms/${encodeURIComponent(tenantSpace.roomId)}/state/m.space.child/${encodeURIComponent(createdRoom.room_id)}`,
            serviceLogin.accessToken,
            {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    via: [serverName()],
                    suggested: true,
                    order: "10",
                }),
            }
        )

        const value = {
            id: metadata.id,
            tenantId: tenant.id,
            tenantName: tenant.name,
            key,
            name,
            topic,
            type: metadata.type,
            entityType: metadata.entityType,
            entityId: metadata.entityId,
            entityUuid: metadata.entityUuid,
            alias: tenantRoomAlias(tenant, key),
            exists: true,
            created: true,
            alreadyExisted: false,
            roomId: createdRoom.room_id,
            parentSpaceRoomId: tenantSpace.roomId,
            invitedUserId: userAccount.matrixUserId,
            serviceUserId: serviceLogin.matrixUserId,
        }

        await markTenantRoomProvisioned(metadata.id, {
            matrixRoomId: value.roomId,
            matrixAlias: value.alias,
            parentSpaceRoomId: value.parentSpaceRoomId,
        })

        matrixTenantRoomCache.set(cacheKey, {
            exists: true,
            cachedUntil: Date.now() + 30 * 60 * 1000,
            value,
        })
        return value
    }

    const matrixUserIdsForInvitees = async (
        currentUserId: string,
        tenantId: number,
        inviteUserIds: string[]
    ) => {
        const uniqueUserIds = Array.from(new Set(inviteUserIds.filter((id) => id && id !== currentUserId)))

        return await Promise.all(uniqueUserIds.map(async (inviteUserId) => {
            const account = await provisionCurrentUser(inviteUserId, tenantId)
            return account.matrixUserId
        }))
    }

    const inviteUsersToRoom = async (roomId: string | null, matrixUserIds: string[]) => {
        if (!roomId || !matrixUserIds.length) return

        const serviceLogin = await ensureServiceAccessToken()

        for (const matrixUserId of matrixUserIds) {
            try {
                await requestMatrixJson(
                    `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`,
                    serviceLogin.accessToken,
                    {
                        method: "POST",
                        headers: { "Content-Type": "application/json" },
                        body: JSON.stringify({ user_id: matrixUserId }),
                    }
                )
            } catch (err: any) {
                if (err.statusCode === 403 || err.statusCode === 400) continue
                throw err
            }
        }
    }

    const ensureServiceUserJoinedRoom = async (room: { roomId?: string | null; alias?: string | null }) => {
        const target = room.roomId || room.alias
        if (!target) return { ok: false, status: "missing_room" }
        if (!belongsToCurrentMatrixServer(target)) {
            return { ok: false, status: "stale_room", roomId: target }
        }

        const serviceLogin = await ensureServiceAccessToken()

        try {
            await requestMatrixJson(
                `/_synapse/admin/v1/join/${encodeURIComponent(target)}`,
                serviceLogin.accessToken,
                {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ user_id: serviceLogin.matrixUserId }),
                }
            )

            return { ok: true, status: "joined_admin", roomId: target }
        } catch (adminErr: any) {
            try {
                await requestMatrixJson(
                    `/_matrix/client/v3/join/${encodeURIComponent(target)}`,
                    serviceLogin.accessToken,
                    {
                        method: "POST",
                        headers: { "Content-Type": "application/json" },
                        body: JSON.stringify({}),
                    }
                )

                return { ok: true, status: "joined_client", roomId: target }
            } catch (clientErr: any) {
                return {
                    ok: false,
                    status: "failed",
                    roomId: target,
                    error: clientErr.message || adminErr.message || "Service-Join fehlgeschlagen",
                }
            }
        }
    }

    const syncServiceJoinedTenantRooms = async () => {
        const rooms = await server.db
            .select({
                id: communicationRooms.id,
                tenantId: communicationRooms.tenantId,
                key: communicationRooms.key,
                name: communicationRooms.name,
                matrixRoomId: communicationRooms.matrixRoomId,
                matrixAlias: communicationRooms.matrixAlias,
            })
            .from(communicationRooms)
            .where(and(
                eq(communicationRooms.archived, false),
                isNotNull(communicationRooms.matrixRoomId)
            ))

        const results = []
        for (const room of rooms) {
            const result = await ensureServiceUserJoinedRoom({
                roomId: room.matrixRoomId,
                alias: room.matrixAlias,
            })
            results.push({
                roomId: room.matrixRoomId,
                roomKey: room.key,
                roomName: room.name,
                ...result,
            })
        }

        return {
            total: results.length,
            joined: results.filter((result) => result.ok).length,
            failed: results.filter((result) => !result.ok).length,
            results,
        }
    }

    const ensureCurrentUserJoinedRoom = async (
        userId: string,
        tenantId: number | null,
        room: { roomId: string, alias: string }
    ) => {
        const session = await createAccessTokenForUser(userId, tenantId)
        const joinCacheKey = `${session.matrixUserId}:${room.roomId || room.alias}`
        const joinedUntil = matrixJoinedRoomCache.get(joinCacheKey)

        if (joinedUntil && joinedUntil > Date.now()) {
            return session
        }

        await requestMatrixJson(
            `/_matrix/client/v3/join/${encodeURIComponent(room.roomId || room.alias)}`,
            session.accessToken,
            {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({}),
            }
        )

        matrixJoinedRoomCache.set(joinCacheKey, Date.now() + 30 * 60 * 1000)
        return session
    }

    const listTenantRooms = async (tenantId: number | null) => {
        const tenant = await getCurrentTenant(tenantId)

        for (const room of defaultTenantRooms) {
            await ensureTenantRoomMetadata(tenant, room)
        }

        const rooms = await server.db
            .select()
            .from(communicationRooms)
            .where(and(
                eq(communicationRooms.tenantId, tenant.id),
                eq(communicationRooms.archived, false)
            ))

        return {
            tenantId: tenant.id,
            tenantName: tenant.name,
            rooms: rooms.map((room) => roomMetadataToApi(tenant, room)).sort((a, b) =>
                String(a.name || a.key).localeCompare(String(b.name || b.key), "de")
            ),
        }
    }

    const getTenantRoomMessages = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {}
    ) => {
        const room = await provisionTenantRoom(userId, tenantId, options)

        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })

        const response = await requestMatrixJson<{
            chunk: MatrixRoomEvent[]
            start?: string
            end?: string
        }>(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/messages?dir=b&limit=50`,
            session.accessToken
        )
        const members = await requestMatrixJson<MatrixJoinedMembersResponse>(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/joined_members`,
            session.accessToken
        )

        const replacementByEventId = new Map<string, MatrixRoomEvent>()
        const reactionsByEventId = new Map<string, Map<string, { key: string; count: number; own: boolean }>>()

        for (const event of response.chunk) {
            const relation = event.content?.["m.relates_to"]

            if (
                event.type === "m.room.message" &&
                relation?.rel_type === "m.replace" &&
                relation.event_id
            ) {
                replacementByEventId.set(relation.event_id, event)
            }

            if (
                event.type === "m.reaction" &&
                relation?.rel_type === "m.annotation" &&
                relation.event_id &&
                relation.key
            ) {
                const eventReactions = reactionsByEventId.get(relation.event_id) || new Map()
                const reaction = eventReactions.get(relation.key) || {
                    key: relation.key,
                    count: 0,
                    own: false,
                }

                reaction.count += 1
                reaction.own = reaction.own || event.sender === session.matrixUserId
                eventReactions.set(relation.key, reaction)
                reactionsByEventId.set(relation.event_id, eventReactions)
            }
        }

        const messages = response.chunk
            .filter((event) =>
                event.type === "m.room.message" &&
                ["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "") &&
                event.content?.["m.relates_to"]?.rel_type !== "m.replace"
            )
            .map((event) => {
                const replacement = replacementByEventId.get(event.event_id)
                const content = replacement?.content?.["m.new_content"] || replacement?.content || event.content

                return {
                    id: event.event_id,
                    sender: event.sender,
                    senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
                    body: content?.body || "",
                    attachment: attachmentFromEvent({ ...event, content }),
                    timestamp: replacement?.origin_server_ts || event.origin_server_ts,
                    own: event.sender === session.matrixUserId,
                    edited: Boolean(replacement),
                    replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
                    reactions: Array.from(reactionsByEventId.get(event.event_id)?.values() || []),
                }
            })
            .reverse()

        return {
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            name: room.name,
            matrixUserId: session.matrixUserId,
            messages,
        }
    }

    const getTenantRoomMembers = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {}
    ) => {
        const room = await provisionTenantRoom(userId, tenantId, options)

        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const members = await requestMatrixJson<MatrixJoinedMembersResponse>(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/joined_members`,
            session.accessToken
        )

        return {
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            name: room.name,
            members: Object.entries(members.joined).map(([matrixUserId, member]) => ({
                matrixUserId,
                displayName: member.display_name || matrixUserId,
                avatarUrl: member.avatar_url || null,
                own: matrixUserId === session.matrixUserId,
            })),
        }
    }

    const searchTenantRoomMessages = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        query: string
    ) => {
        const searchTerm = query.trim()

        if (searchTerm.length < 2) {
            return {
                roomId: "",
                alias: "",
                key: options.key || "allgemein",
                name: options.name || options.key || "Chat",
                count: 0,
                results: [],
            }
        }

        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const [response, members] = await Promise.all([
            requestMatrixJson<MatrixRoomSearchResponse>(
                "/_matrix/client/v3/search",
                session.accessToken,
                {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                        search_categories: {
                            room_events: {
                                search_term: searchTerm,
                                keys: ["content.body"],
                                order_by: "recent",
                                filter: {
                                    limit: 25,
                                    rooms: [room.roomId],
                                },
                            },
                        },
                    }),
                }
            ),
            requestMatrixJson<MatrixJoinedMembersResponse>(
                `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/joined_members`,
                session.accessToken
            ),
        ])

        const roomEvents = response.search_categories?.room_events
        const results = (roomEvents?.results || [])
            .map((item) => item.result)
            .filter((event): event is MatrixRoomEvent & { room_id?: string } =>
                Boolean(
                    event?.event_id &&
                    event.type === "m.room.message" &&
                    ["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "")
                )
            )
            .map((event) => ({
                id: event.event_id,
                roomId: event.room_id || room.roomId,
                key: room.key,
                sender: event.sender,
                senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
                body: event.content?.body || "",
                attachment: attachmentFromEvent(event),
                timestamp: event.origin_server_ts,
                own: event.sender === session.matrixUserId,
            }))

        return {
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            name: room.name,
            count: roomEvents?.count || results.length,
            results,
        }
    }

    const syncTenantRoomEvents = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        since?: string,
        initial = false
    ) => {
        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const filter = {
            room: {
                rooms: [room.roomId],
                timeline: {
                    limit: 30,
                },
            },
            presence: {
                types: [],
            },
            account_data: {
                types: [],
            },
        }
        const params = new URLSearchParams({
            timeout: since && !initial ? "25000" : "0",
            filter: JSON.stringify(filter),
        })

        if (since) params.set("since", since)

        const response = await requestMatrixJson<MatrixSyncResponse>(
            `/_matrix/client/v3/sync?${params.toString()}`,
            session.accessToken
        )
        const joinedRoom = response.rooms?.join?.[room.roomId]
        const timelineEvents = joinedRoom?.timeline?.events || []
        const stateEvents = joinedRoom?.state?.events || []

        if (initial) {
            return {
                roomId: room.roomId,
                alias: room.alias,
                key: room.key,
                name: room.name,
                nextBatch: response.next_batch || since || "",
                messages: [],
                replacements: [],
                reactions: [],
                redactions: [],
                membersChanged: false,
            }
        }

        const messages = timelineEvents
            .filter((event) =>
                event.type === "m.room.message" &&
                ["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "") &&
                event.content?.["m.relates_to"]?.rel_type !== "m.replace"
            )
            .map((event) => ({
                id: event.event_id,
                sender: event.sender,
                senderDisplayName: event.sender,
                body: event.content?.body || "",
                attachment: attachmentFromEvent(event),
                timestamp: event.origin_server_ts,
                own: event.sender === session.matrixUserId,
                replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
                reactions: [],
            }))

        const replacements = timelineEvents
            .filter((event) =>
                event.type === "m.room.message" &&
                event.content?.["m.relates_to"]?.rel_type === "m.replace" &&
                Boolean(event.content?.["m.relates_to"]?.event_id)
            )
            .map((event) => ({
                id: event.event_id,
                targetEventId: event.content?.["m.relates_to"]?.event_id,
                body: event.content?.["m.new_content"]?.body || event.content?.body || "",
                timestamp: event.origin_server_ts,
                sender: event.sender,
                own: event.sender === session.matrixUserId,
            }))

        const reactions = timelineEvents
            .filter((event) =>
                event.type === "m.reaction" &&
                event.content?.["m.relates_to"]?.rel_type === "m.annotation" &&
                Boolean(event.content?.["m.relates_to"]?.event_id) &&
                Boolean(event.content?.["m.relates_to"]?.key)
            )
            .map((event) => ({
                id: event.event_id,
                targetEventId: event.content?.["m.relates_to"]?.event_id,
                key: event.content?.["m.relates_to"]?.key,
                sender: event.sender,
                own: event.sender === session.matrixUserId,
            }))

        const redactions = timelineEvents
            .filter((event) => event.type === "m.room.redaction" && Boolean(event.redacts))
            .map((event) => ({
                id: event.event_id,
                targetEventId: event.redacts,
                sender: event.sender,
                timestamp: event.origin_server_ts,
            }))

        return {
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            name: room.name,
            nextBatch: response.next_batch || since || "",
            messages,
            replacements,
            reactions,
            redactions,
            membersChanged: [...timelineEvents, ...stateEvents].some((event) => event.type === "m.room.member"),
        }
    }

    const syncServiceRoomEvents = async (since?: string, initial = false) => {
        const service = await ensureServiceAccessToken()
        const filter = {
            room: {
                timeline: {
                    limit: 50,
                },
            },
            presence: {
                types: [],
            },
            account_data: {
                types: [],
            },
        }
        const params = new URLSearchParams({
            timeout: since && !initial ? "25000" : "0",
            filter: JSON.stringify(filter),
        })

        if (since) params.set("since", since)

        const response = await requestMatrixJson<MatrixSyncResponse>(
            `/_matrix/client/v3/sync?${params.toString()}`,
            service.accessToken
        )
        const joinedRooms = response.rooms?.join || {}

        return {
            nextBatch: response.next_batch || since || "",
            serviceUserId: service.matrixUserId,
            rooms: Object.entries(joinedRooms).map(([roomId, joinedRoom]) => {
                const timelineEvents = joinedRoom.timeline?.events || []
                const messages = initial
                    ? []
                    : timelineEvents
                        .filter((event) =>
                            event.type === "m.room.message" &&
                            ["m.text", "m.file", "m.image"].includes(event.content?.msgtype || "") &&
                            event.content?.["m.relates_to"]?.rel_type !== "m.replace"
                        )
                        .map((event) => ({
                            id: event.event_id,
                            roomId,
                            sender: event.sender,
                            senderDisplayName: event.sender,
                            body: event.content?.body || "",
                            attachment: attachmentFromEvent(event),
                            timestamp: event.origin_server_ts,
                            own: event.sender === service.matrixUserId,
                            replyToEventId: event.content?.["m.relates_to"]?.["m.in_reply_to"]?.event_id || null,
                        }))

                return {
                    roomId,
                    messages,
                    membersChanged: [...timelineEvents, ...(joinedRoom.state?.events || [])]
                        .some((event) => event.type === "m.room.member"),
                }
            }),
        }
    }

    const sendTenantRoomMessage = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        text: string,
        messageOptions: MatrixMessageOptions = {}
    ) => {
        const message = text.trim()

        if (!message) {
            throw Object.assign(
                new Error("Message text is required"),
                { statusCode: 400 }
            )
        }

        const room = await provisionTenantRoom(userId, tenantId, options)

        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`

        const content: Record<string, any> = {
            msgtype: "m.text",
            body: message,
        }

        if (messageOptions.replyToEventId) {
            content["m.relates_to"] = {
                "m.in_reply_to": {
                    event_id: messageOptions.replyToEventId,
                },
            }
        }

        const response = await requestMatrixJson<{ event_id: string }>(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
            session.accessToken,
            {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(content),
            }
        )

        return {
            id: response.event_id,
            sender: session.matrixUserId,
            senderDisplayName: await getCurrentUserDisplayName(userId, tenantId),
            body: message,
            timestamp: Date.now(),
            own: true,
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            replyToEventId: messageOptions.replyToEventId || null,
            reactions: [],
        }
    }

    const sendTenantRoomReaction = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        eventId: string,
        key: string
    ) => {
        const reactionKey = key.trim()
        if (!eventId || !reactionKey) {
            throw Object.assign(
                new Error("Reaction target and key are required"),
                { statusCode: 400 }
            )
        }

        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
        await requestMatrixJson<{ event_id: string }>(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.reaction/${encodeURIComponent(txnId)}`,
            session.accessToken,
            {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    "m.relates_to": {
                        rel_type: "m.annotation",
                        event_id: eventId,
                        key: reactionKey,
                    },
                }),
            }
        )

        return { success: true, eventId, key: reactionKey }
    }

    const editTenantRoomMessage = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        eventId: string,
        text: string
    ) => {
        const message = text.trim()
        if (!eventId || !message) {
            throw Object.assign(
                new Error("Nachricht und Zielnachricht sind erforderlich"),
                { statusCode: 400 }
            )
        }

        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
        const response = await requestMatrixJson<{ event_id: string }>(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
            session.accessToken,
            {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    msgtype: "m.text",
                    body: `* ${message}`,
                    "m.new_content": {
                        msgtype: "m.text",
                        body: message,
                    },
                    "m.relates_to": {
                        rel_type: "m.replace",
                        event_id: eventId,
                    },
                }),
            }
        )

        return {
            id: response.event_id,
            targetEventId: eventId,
            body: message,
            timestamp: Date.now(),
            own: true,
        }
    }

    const redactTenantRoomMessage = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        eventId: string
    ) => {
        if (!eventId) {
            throw Object.assign(
                new Error("Zielnachricht ist erforderlich"),
                { statusCode: 400 }
            )
        }

        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
        await requestMatrixJson(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`,
            session.accessToken,
            {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    reason: "Nachricht in FEDEO gelöscht",
                }),
            }
        )

        return { success: true, eventId }
    }

    const markTenantRoomRead = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        eventId: string
    ) => {
        if (!eventId) return { success: true, skipped: true }

        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })

        await requestMatrixJson(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/receipt/m.read/${encodeURIComponent(eventId)}`,
            session.accessToken,
            {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({}),
            }
        )

        return { success: true, eventId }
    }

    const sendTenantRoomAttachment = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        attachment: MatrixAttachmentInput
    ) => {
        if (!attachment.buffer?.length) {
            throw Object.assign(
                new Error("Attachment file is required"),
                { statusCode: 400 }
            )
        }

        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const upload = await requestMatrixJson<{ content_uri: string }>(
            `/_matrix/media/v3/upload?filename=${encodeURIComponent(attachment.filename)}`,
            session.accessToken,
            {
                method: "POST",
                headers: { "Content-Type": attachment.mimeType || "application/octet-stream" },
                body: attachment.buffer as any,
            }
        )
        const txnId = `${Date.now()}-${randomBytes(8).toString("hex")}`
        const isImage = (attachment.mimeType || "").startsWith("image/")
        const messageContent = {
            msgtype: isImage ? "m.image" : "m.file",
            body: attachment.filename,
            url: upload.content_uri,
            info: {
                mimetype: attachment.mimeType || "application/octet-stream",
                size: attachment.size,
            },
        }
        const response = await requestMatrixJson<{ event_id: string }>(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
            session.accessToken,
            {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(messageContent),
            }
        )

        return {
            id: response.event_id,
            sender: session.matrixUserId,
            senderDisplayName: await getCurrentUserDisplayName(userId, tenantId),
            body: attachment.filename,
            attachment: {
                type: isImage ? "image" : "file",
                url: upload.content_uri,
                fileName: attachment.filename,
                mimeType: attachment.mimeType || "application/octet-stream",
                size: attachment.size,
                previewUrl: isImage ? matrixMediaUrl(upload.content_uri) : null,
                downloadUrl: matrixMediaUrl(upload.content_uri),
                isImage,
            },
            timestamp: Date.now(),
            own: true,
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
        }
    }

    const getMediaContent = async (
        userId: string,
        tenantId: number | null,
        mxcUri: string
    ) => {
        const session = await createAccessTokenForUser(userId, tenantId)
        const response = await fetch(matrixMediaUrl(mxcUri), {
            headers: {
                Authorization: `Bearer ${session.accessToken}`,
            },
        })

        if (!response.ok) {
            throw Object.assign(
                new Error(`Matrix media request failed with ${response.status}`),
                { statusCode: response.status }
            )
        }

        return {
            buffer: Buffer.from(await response.arrayBuffer()),
            contentType: response.headers.get("content-type") || "application/octet-stream",
            contentLength: response.headers.get("content-length"),
        }
    }

    const createElementRoomSession = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {}
    ) => {
        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })

        const token = await requestMatrixJson<MatrixLoginTokenResponse>(
            "/_matrix/client/v1/login/get_token",
            session.accessToken,
            {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({}),
            }
        )

        return {
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            name: room.name,
            matrixUserId: session.matrixUserId,
            loginToken: token.login_token,
            expiresInMs: token.expires_in_ms,
            homeserverUrl: homeserverUrl(),
            serverName: serverName(),
        }
    }

    const createLiveKitRoomSession = async (
        userId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {}
    ) => {
        if (!livekitKey() || !livekitSecret()) {
            throw Object.assign(
                new Error("LIVEKIT_KEY and LIVEKIT_SECRET are not configured"),
                { statusCode: 503 }
            )
        }

        const room = await provisionTenantRoom(userId, tenantId, options)
        const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })
        const displayName = await getCurrentUserDisplayName(userId, tenantId)
        const liveKitRoomName = `fedeo-${tenantId || "global"}-${room.key}`.replace(/[^a-zA-Z0-9_-]/g, "_")
        const now = Math.floor(Date.now() / 1000)
        const expiresInSeconds = 60 * 60
        const video: LiveKitGrant = {
            roomJoin: true,
            room: liveKitRoomName,
            canPublish: true,
            canSubscribe: true,
        }
        const token = jwt.sign(
            {
                sub: session.matrixUserId,
                name: displayName,
                video,
                nbf: now - 10,
                exp: now + expiresInSeconds,
            },
            livekitSecret(),
            {
                algorithm: "HS256",
                issuer: livekitKey(),
            }
        )

        return {
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            name: room.name,
            matrixUserId: session.matrixUserId,
            displayName,
            liveKitUrl: livekitUrl(),
            liveKitRoomName,
            liveKitToken: token,
            expiresInMs: expiresInSeconds * 1000,
        }
    }

    const listTenantCommunicationUsers = async (tenantId: number | null) => {
        const tenant = await getCurrentTenant(tenantId)
        const rows = await server.db
            .select({
                userId: authTenantUsers.user_id,
                email: authUsers.email,
                profileActive: authProfiles.active,
                firstName: authProfiles.first_name,
                lastName: authProfiles.last_name,
                fullName: authProfiles.full_name,
            })
            .from(authTenantUsers)
            .innerJoin(authUsers, eq(authUsers.id, authTenantUsers.user_id))
            .leftJoin(authProfiles, and(
                eq(authProfiles.user_id, authTenantUsers.user_id),
                eq(authProfiles.tenant_id, tenant.id)
            ))
            .where(eq(authTenantUsers.tenant_id, tenant.id))

        const users = rows
            .filter((row) => row.profileActive !== false)
            .map((row) => ({
                userId: row.userId,
                email: row.email,
                displayName: row.fullName || `${row.firstName || ""} ${row.lastName || ""}`.trim() || row.email,
            }))

        return await Promise.all(users.map(async (user) => ({
            ...user,
            matrixUserId: await matrixUserIdForUser(user.userId, tenant.id),
        })))
    }

    const inviteMatrixUserToRoom = async (
        room: { roomId: string },
        matrixUserId: string,
        reason?: string
    ) => {
        const serviceLogin = await ensureServiceAccessToken()

        try {
            await requestMatrixJson(
                `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/invite`,
                serviceLogin.accessToken,
                {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                        user_id: matrixUserId,
                        reason,
                    }),
                }
            )

            return "invited"
        } catch (err: any) {
            if (
                err.errcode === "M_FORBIDDEN" ||
                err.errcode === "M_BAD_STATE" ||
                err.errcode === "M_UNKNOWN"
            ) {
                return "already_available"
            }

            throw err
        }
    }

    const syncTenantRoomMembers = async (
        requestingUserId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {}
    ) => {
        const room = await provisionTenantRoom(requestingUserId, tenantId, options)
        const users = await listTenantCommunicationUsers(tenantId)
        const results = []

        for (const user of users) {
            try {
                const account = await provisionCurrentUser(user.userId, tenantId)
                await inviteMatrixUserToRoom(
                    { roomId: room.roomId },
                    account.matrixUserId,
                    `FEDEO-Raumsynchronisation: ${room.name}`
                )
                await ensureCurrentUserJoinedRoom(user.userId, tenantId, {
                    roomId: room.roomId,
                    alias: room.alias,
                })

                results.push({
                    userId: user.userId,
                    email: user.email,
                    displayName: user.displayName,
                    matrixUserId: account.matrixUserId,
                    status: "joined",
                    ok: true,
                })
            } catch (err: any) {
                results.push({
                    userId: user.userId,
                    email: user.email,
                    displayName: user.displayName,
                    status: "failed",
                    ok: false,
                    error: err.message || "Synchronisation fehlgeschlagen",
                })
            }
        }

        return {
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            name: room.name,
            total: results.length,
            joined: results.filter((item) => item.status === "joined").length,
            invited: results.filter((item) => item.status === "invited").length,
            alreadyAvailable: results.filter((item) => item.status === "already_available").length,
            failed: results.filter((item) => !item.ok).length,
            results,
        }
    }

    const inviteTenantRoomMember = async (
        requestingUserId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        targetUserId: string
    ) => {
        if (!targetUserId) {
            throw Object.assign(
                new Error("Benutzer ist erforderlich"),
                { statusCode: 400 }
            )
        }

        const users = await listTenantCommunicationUsers(tenantId)
        const targetUser = users.find((user) => user.userId === targetUserId)

        if (!targetUser) {
            throw Object.assign(
                new Error("Benutzer gehört nicht zum aktiven Mandanten"),
                { statusCode: 404 }
            )
        }

        const room = await provisionTenantRoom(requestingUserId, tenantId, options)
        const account = await provisionCurrentUser(targetUser.userId, tenantId)
        const inviteStatus = await inviteMatrixUserToRoom(
            { roomId: room.roomId },
            account.matrixUserId,
            `FEDEO-Einladung: ${room.name}`
        )
        await ensureCurrentUserJoinedRoom(targetUser.userId, tenantId, {
            roomId: room.roomId,
            alias: room.alias,
        })

        return {
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            name: room.name,
            userId: targetUser.userId,
            email: targetUser.email,
            displayName: targetUser.displayName,
            matrixUserId: account.matrixUserId,
            status: inviteStatus === "invited" ? "joined" : inviteStatus,
            ok: true,
        }
    }

    const removeTenantRoomMember = async (
        requestingUserId: string,
        tenantId: number | null,
        options: MatrixTenantRoomOptions = {},
        matrixUserId: string
    ) => {
        if (!matrixUserId) {
            throw Object.assign(
                new Error("Matrix-Benutzer ist erforderlich"),
                { statusCode: 400 }
            )
        }

        const room = await provisionTenantRoom(requestingUserId, tenantId, options)
        const session = await createAccessTokenForUser(requestingUserId, tenantId)

        if (matrixUserId === session.matrixUserId) {
            throw Object.assign(
                new Error("Du kannst dich nicht selbst aus dem Raum entfernen"),
                { statusCode: 400 }
            )
        }

        const serviceLogin = await ensureServiceAccessToken()
        await requestMatrixJson(
            `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/kick`,
            serviceLogin.accessToken,
            {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    user_id: matrixUserId,
                    reason: "FEDEO-Mitgliederverwaltung",
                }),
            }
        )

        matrixJoinedRoomCache.delete(`${matrixUserId}:${room.roomId}`)
        return {
            success: true,
            roomId: room.roomId,
            alias: room.alias,
            key: room.key,
            matrixUserId,
        }
    }

    const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
        getTenantRoomMessages(userId, tenantId, {
            key: "allgemein",
            name: "Allgemeiner Chat",
        })

    const getGeneralRoomMembers = (userId: string, tenantId: number | null) =>
        getTenantRoomMembers(userId, tenantId, {
            key: "allgemein",
            name: "Allgemeiner Chat",
        })

    const sendGeneralRoomMessage = (
        userId: string,
        tenantId: number | null,
        text: string,
        messageOptions: MatrixMessageOptions = {}
    ) =>
        sendTenantRoomMessage(
            userId,
            tenantId,
            {
                key: "allgemein",
                name: "Allgemeiner Chat",
            },
            text,
            messageOptions
        )

    const sendGeneralRoomAttachment = (userId: string, tenantId: number | null, attachment: MatrixAttachmentInput) =>
        sendTenantRoomAttachment(
            userId,
            tenantId,
            {
                key: "allgemein",
                name: "Allgemeiner Chat",
            },
            attachment
        )

    return {
        getStatus,
        matrixUserIdForUser,
        getCurrentUserDisplayName,
        provisionCurrentUser,
        getTenantSpaceStatus,
        provisionCurrentTenantSpace,
        listTenantRooms,
        listTenantCommunicationUsers,
        getTenantRoomStatus,
        provisionTenantRoom,
        createAccessTokenForUser,
        getTenantRoomMessages,
        getTenantRoomMembers,
        searchTenantRoomMessages,
        syncTenantRoomEvents,
        syncServiceRoomEvents,
        syncServiceJoinedTenantRooms,
        sendTenantRoomMessage,
        sendTenantRoomReaction,
        editTenantRoomMessage,
        redactTenantRoomMessage,
        markTenantRoomRead,
        sendTenantRoomAttachment,
        getMediaContent,
        createElementRoomSession,
        createLiveKitRoomSession,
        inviteTenantRoomMember,
        removeTenantRoomMember,
        syncTenantRoomMembers,
        getGeneralRoomMessages,
        getGeneralRoomMembers,
        sendGeneralRoomMessage,
        sendGeneralRoomAttachment,
    }
}
