import { createHash } from "node:crypto"
import type { FastifyInstance } from "fastify"
import { and, desc, eq, inArray, isNotNull, ne } from "drizzle-orm"
import { authProfiles, authTenantUsers, authUsers, communicationRooms, notificationsItems } from "../../db/schema"
import { matrixService } from "./matrix.service"
import { NotificationService, UserDirectory } from "./notification.service"

type ChatRecipient = {
    userId: string
    email?: string | null
    firstName?: string | null
    lastName?: string | null
    fullName?: string | null
    matrixUserId?: string
}

type MatrixPushWorkerEvent = {
    at: string
    type: string
    roomKey?: string
    roomId?: string | null
    messageId?: string
    sender?: string
    targets?: number
    created?: number
    delivered?: number
    failed?: number
    error?: string
}

const matrixPushWorkerState = {
    enabled: false,
    startedAt: null as string | null,
    lastRunAt: null as string | null,
    lastJoinAt: null as string | null,
    lastJoinTotal: 0,
    lastJoinJoined: 0,
    lastJoinFailed: 0,
    hasSyncToken: false,
    lastSyncRooms: 0,
    lastSyncMessages: 0,
    lastMatchedRooms: 0,
    lastNotificationsCreated: 0,
    lastNotificationsDelivered: 0,
    lastNotificationsFailed: 0,
    lastError: null as string | null,
    events: [] as MatrixPushWorkerEvent[],
}

const rememberWorkerEvent = (event: MatrixPushWorkerEvent) => {
    matrixPushWorkerState.events = [
        {
            at: new Date().toISOString(),
            ...event,
        },
        ...matrixPushWorkerState.events,
    ].slice(0, 25)
}

export const getMatrixPushWorkerState = () => ({
    ...matrixPushWorkerState,
    events: [...matrixPushWorkerState.events],
})

const getUserDirectory: UserDirectory = async (server: FastifyInstance, userId) => {
    const rows = await server.db
        .select({ email: authUsers.email })
        .from(authUsers)
        .where(eq(authUsers.id, userId))
        .limit(1)

    return rows[0] || null
}

const displayUserName = (user: { fullName?: string | null; firstName?: string | null; lastName?: string | null; email?: string | null }) => {
    const name = user.fullName || [user.firstName, user.lastName].filter(Boolean).join(" ")
    return name || user.email || "Benutzer"
}

const directRoomKey = (firstUserId: string, secondUserId: string) => {
    const hash = createHash("sha256")
        .update([firstUserId, secondUserId].sort().join(":"))
        .digest("hex")
        .slice(0, 16)

    return `direct_${hash}`
}

const mentionAliasesForUser = (user: ChatRecipient) => {
    const name = displayUserName(user)
    return Array.from(new Set([
        name,
        user.fullName,
        [user.firstName, user.lastName].filter(Boolean).join(" "),
        user.firstName,
        user.email,
    ].filter(Boolean).map((value) => String(value).toLowerCase())))
}

const mentionedRecipientIds = (text: string, recipients: ChatRecipient[]) => {
    const normalizedText = text.toLowerCase()

    return recipients
        .filter((recipient) => mentionAliasesForUser(recipient).some((alias) =>
            normalizedText.includes(`@${alias}`)
        ))
        .map((recipient) => recipient.userId)
}

export function startMatrixPushWorker(server: FastifyInstance) {
    if (process.env.MATRIX_PUSH_WORKER_DISABLED === "1") {
        server.log.info("Matrix-Push-Worker ist deaktiviert")
        return
    }

    matrixPushWorkerState.enabled = true
    matrixPushWorkerState.startedAt = new Date().toISOString()
    rememberWorkerEvent({ at: new Date().toISOString(), type: "started" })

    const matrix = matrixService(server)
    const notifications = new NotificationService(server, getUserDirectory)
    const intervalMs = Math.max(Number(process.env.MATRIX_PUSH_WORKER_INTERVAL_MS || 3000), 1000)
    let since: string | undefined
    let running = false
    let stopped = false
    let timer: ReturnType<typeof setTimeout> | undefined
    let lastServiceJoinSyncAt = 0
    let errorBackoffMs = 0

    const getTenantRecipients = async (tenantId: number) => {
        const rows = await server.db
            .select({
                userId: authTenantUsers.user_id,
                email: authUsers.email,
                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, tenantId)
            ))
            .where(eq(authTenantUsers.tenant_id, tenantId))

        return await Promise.all(rows.map(async (row) => ({
            ...row,
            matrixUserId: await matrix.matrixUserIdForUser(row.userId, tenantId),
        })))
    }

    const hasChatNotificationForMessage = async (tenantId: number, userId: string, messageId: string) => {
        const rows = await server.db
            .select({
                payload: notificationsItems.payload,
            })
            .from(notificationsItems)
            .where(and(
                eq(notificationsItems.tenantId, tenantId),
                eq(notificationsItems.userId, userId),
                eq(notificationsItems.eventType, "communication.message.new")
            ))
            .orderBy(desc(notificationsItems.createdAt))
            .limit(200)

        return rows.some((row) => (row.payload as any)?.messageId === messageId)
    }

    const recipientsForMessage = (
        room: typeof communicationRooms.$inferSelect,
        recipients: ChatRecipient[],
        senderUserId: string | null,
        text: string
    ) => {
        const candidates = senderUserId
            ? recipients.filter((recipient) => recipient.userId !== senderUserId)
            : recipients
        const mentioned = new Set(mentionedRecipientIds(text, candidates))
        const directRecipients = new Set<string>()

        if (room.type === "direct" && room.entityUuid && room.entityUuid !== senderUserId) {
            directRecipients.add(room.entityUuid)
        } else if (room.type === "direct" && senderUserId) {
            candidates
                .filter((recipient) => directRoomKey(senderUserId, recipient.userId) === room.key)
                .forEach((recipient) => directRecipients.add(recipient.userId))
        }

        return candidates
            .filter((recipient) => directRecipients.has(recipient.userId) || mentioned.has(recipient.userId))
            .map((recipient) => ({
                ...recipient,
                mentioned: mentioned.has(recipient.userId),
                direct: directRecipients.has(recipient.userId),
            }))
    }

    const deliverMessageNotification = async (
        room: typeof communicationRooms.$inferSelect,
        message: any,
        recipients: ChatRecipient[]
    ) => {
        if (!message.id || message.own) return

        const sender = recipients.find((recipient) => recipient.matrixUserId === message.sender) || null
        const text = message.body || message.attachment?.fileName || "Neue Nachricht"
        const targets = recipientsForMessage(room, recipients, sender?.userId || null, text)
        rememberWorkerEvent({
            at: new Date().toISOString(),
            type: "message_seen",
            roomKey: room.key,
            roomId: room.matrixRoomId,
            messageId: message.id,
            sender: message.sender,
            targets: targets.length,
        })
        if (!targets.length) return

        const senderName = sender ? displayUserName(sender) : message.senderDisplayName || message.sender || "Matrix"
        const preview = text.length > 160 ? `${text.slice(0, 157)}...` : text

        for (const target of targets) {
            if (await hasChatNotificationForMessage(room.tenantId, target.userId, message.id)) {
                rememberWorkerEvent({
                    at: new Date().toISOString(),
                    type: "notification_skipped_duplicate",
                    roomKey: room.key,
                    roomId: room.matrixRoomId,
                    messageId: message.id,
                    sender: message.sender,
                    targets: 1,
                })
                continue
            }

            const result = await notifications.trigger({
                tenantId: room.tenantId,
                userId: target.userId,
                eventType: "communication.message.new",
                title: target.mentioned ? `${senderName} hat dich erwähnt` : `Neue Direktnachricht von ${senderName}`,
                message: preview,
                payload: {
                    link: `/communication/chat?room=${encodeURIComponent(room.key)}`,
                    roomKey: room.key,
                    roomName: room.name,
                    roomType: room.type,
                    messageId: message.id,
                    matrixSender: message.sender,
                    mentioned: target.mentioned,
                    direct: target.direct,
                },
                channels: ["inapp", "push"],
            })
            matrixPushWorkerState.lastNotificationsCreated += result.created || 0
            matrixPushWorkerState.lastNotificationsDelivered += result.delivered || 0
            matrixPushWorkerState.lastNotificationsFailed += result.failed || 0
            rememberWorkerEvent({
                at: new Date().toISOString(),
                type: "notification_triggered",
                roomKey: room.key,
                roomId: room.matrixRoomId,
                messageId: message.id,
                sender: message.sender,
                targets: 1,
                created: result.created || 0,
                delivered: result.delivered || 0,
                failed: result.failed || 0,
            })
        }
    }

    const runOnce = async () => {
        if (running || stopped) return
        running = true

        try {
            matrixPushWorkerState.lastRunAt = new Date().toISOString()
            matrixPushWorkerState.lastError = null
            matrixPushWorkerState.lastSyncRooms = 0
            matrixPushWorkerState.lastSyncMessages = 0
            matrixPushWorkerState.lastMatchedRooms = 0
            matrixPushWorkerState.lastNotificationsCreated = 0
            matrixPushWorkerState.lastNotificationsDelivered = 0
            matrixPushWorkerState.lastNotificationsFailed = 0

            if (!lastServiceJoinSyncAt || Date.now() - lastServiceJoinSyncAt > 60_000) {
                const joinResult = await matrix.syncServiceJoinedTenantRooms()
                lastServiceJoinSyncAt = Date.now()
                matrixPushWorkerState.lastJoinAt = new Date().toISOString()
                matrixPushWorkerState.lastJoinTotal = joinResult.total
                matrixPushWorkerState.lastJoinJoined = joinResult.joined
                matrixPushWorkerState.lastJoinFailed = joinResult.failed
                rememberWorkerEvent({
                    at: new Date().toISOString(),
                    type: "service_join_sync",
                    targets: joinResult.total,
                    delivered: joinResult.joined,
                    failed: joinResult.failed,
                })
                if (joinResult.failed) {
                    console.warn("Matrix-Push-Worker: Service-User konnte nicht alle Räume joinen", {
                        total: joinResult.total,
                        joined: joinResult.joined,
                        failed: joinResult.failed,
                    })
                }
            }

            const initial = !since
            const sync = await matrix.syncServiceRoomEvents(since, initial)
            since = sync.nextBatch || since
            matrixPushWorkerState.hasSyncToken = Boolean(since)
            matrixPushWorkerState.lastSyncRooms = sync.rooms?.length || 0
            matrixPushWorkerState.lastSyncMessages = (sync.rooms || [])
                .reduce((sum: number, room: any) => sum + (room.messages?.length || 0), 0)

            if (!initial && sync.rooms?.length) {
                const roomIds = sync.rooms.map((room: any) => room.roomId).filter(Boolean)
                const rooms = roomIds.length
                    ? await server.db
                        .select()
                        .from(communicationRooms)
                        .where(and(
                            inArray(communicationRooms.matrixRoomId, roomIds),
                            ne(communicationRooms.archived, true),
                            isNotNull(communicationRooms.matrixRoomId)
                        ))
                    : []
                const roomsByMatrixId = new Map(rooms.map((room) => [room.matrixRoomId, room]))
                matrixPushWorkerState.lastMatchedRooms = rooms.length
                const recipientsByTenant = new Map<number, ChatRecipient[]>()

                for (const syncedRoom of sync.rooms) {
                    const room = roomsByMatrixId.get(syncedRoom.roomId)
                    if (!room || !syncedRoom.messages?.length) continue

                    if (!recipientsByTenant.has(room.tenantId)) {
                        recipientsByTenant.set(room.tenantId, await getTenantRecipients(room.tenantId))
                    }

                    const recipients = recipientsByTenant.get(room.tenantId) || []
                    for (const message of syncedRoom.messages) {
                        await deliverMessageNotification(room, message, recipients)
                    }
                }
            }
            errorBackoffMs = 0
        } catch (err) {
            matrixPushWorkerState.lastError = err instanceof Error ? err.message : String(err)
            const retryAfterMs = Number((err as any)?.retryAfterMs || (err as any)?.body?.retry_after_ms || 0)
            errorBackoffMs = Math.min(
                Math.max(retryAfterMs || (errorBackoffMs ? errorBackoffMs * 2 : 30_000), 30_000),
                5 * 60_000
            )
            rememberWorkerEvent({
                at: new Date().toISOString(),
                type: "error",
                error: matrixPushWorkerState.lastError,
            })
            console.error("Matrix-Push-Worker konnte Matrix-Events nicht verarbeiten", err)
            server.log.error({ err }, "Matrix-Push-Worker konnte Matrix-Events nicht verarbeiten")
        } finally {
            running = false
            if (!stopped) {
                const nextDelay = errorBackoffMs || (since ? 0 : intervalMs)
                timer = setTimeout(() => void runOnce(), nextDelay)
            }
        }
    }

    timer = setTimeout(() => void runOnce(), intervalMs)
    server.addHook("onClose", async () => {
        stopped = true
        if (timer) clearTimeout(timer)
    })
}
