import type { FastifyInstance } from "fastify"
import webPush from "web-push"
import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm"
import {
    authUsers,
    notificationMobilePushDevices,
    notificationPushSubscriptions,
    notificationsEventTypes,
    notificationsItems,
    notificationsPreferences,
    notificationsPreferencesDefaults,
} from "../../db/schema"
import { secrets } from "../utils/secrets"
import { pushServerClient } from "./push-server.client"

export type NotificationChannel = "inapp" | "email" | "push" | "webhook" | "sms"
export type NotificationStatus = "queued" | "sent" | "failed" | "read"

export interface TriggerInput {
    tenantId: number
    userId?: string
    userIds?: string[]
    eventType: string
    title: string
    message: string
    payload?: Record<string, unknown>
    channels?: NotificationChannel[]
}

export interface PushSubscriptionInput {
    endpoint: string
    keys: {
        p256dh: string
        auth: string
    }
    deviceLabel?: string
    meta?: Record<string, unknown>
}

export interface UserDirectoryInfo {
    email?: string
}

export type UserDirectory = (
    server: FastifyInstance,
    userId: string,
    tenantId: number
) => Promise<UserDirectoryInfo | null>

const DEFAULT_CHANNELS: NotificationChannel[] = ["inapp"]

export class NotificationService {
    constructor(
        private server: FastifyInstance,
        private getUser: UserDirectory
    ) {}

    async trigger(input: TriggerInput) {
        const tenantId = input.tenantId
        const userIds = Array.from(new Set([...(input.userIds || []), input.userId].filter(Boolean))) as string[]

        if (!tenantId) throw new Error("tenantId fehlt")
        if (!userIds.length) throw new Error("Keine Empfänger angegeben")

        const eventType = await this.getActiveEventType(input.eventType)
        const allowedChannels = this.normalizeChannels(eventType.allowedChannels)
        const requestedChannels = input.channels?.length ? input.channels : allowedChannels
        const channels = requestedChannels.filter((channel) => allowedChannels.includes(channel))

        if (!channels.length) {
            return { success: true, created: 0, delivered: 0, skipped: userIds.length }
        }

        const results = []

        for (const userId of userIds) {
            const enabledChannels = await this.resolveEnabledChannels({
                tenantId,
                userId,
                eventType: input.eventType,
                channels,
            })

            for (const channel of enabledChannels) {
                const itemRows = await this.server.db
                    .insert(notificationsItems)
                    .values({
                        tenantId,
                        userId,
                        eventType: input.eventType,
                        title: input.title,
                        message: input.message,
                        payload: input.payload ?? null,
                        channel,
                        status: "queued",
                    })
                    .returning()

                const item = itemRows[0]
                if (!item) continue

                results.push(await this.deliver(item))
            }
        }

        return {
            success: results.every((result) => result.success),
            created: results.length,
            delivered: results.filter((result) => result.success).length,
            failed: results.filter((result) => !result.success).length,
        }
    }

    async listForUser(tenantId: number, userId: string, limit = 50) {
        return await this.server.db
            .select()
            .from(notificationsItems)
            .where(and(
                eq(notificationsItems.tenantId, tenantId),
                eq(notificationsItems.userId, userId),
                eq(notificationsItems.channel, "inapp")
            ))
            .orderBy(desc(notificationsItems.createdAt))
            .limit(Math.min(Math.max(limit, 1), 100))
    }

    async markRead(tenantId: number, userId: string, notificationId: string) {
        const rows = await this.server.db
            .update(notificationsItems)
            .set({ readAt: new Date(), status: "read" })
            .where(and(
                eq(notificationsItems.id, notificationId),
                eq(notificationsItems.tenantId, tenantId),
                eq(notificationsItems.userId, userId)
            ))
            .returning()

        return rows[0] || null
    }

    async registerPushSubscription(
        tenantId: number,
        userId: string,
        subscription: PushSubscriptionInput,
        userAgent?: string
    ) {
        if (!subscription.endpoint || !subscription.keys?.p256dh || !subscription.keys?.auth) {
            throw new Error("Push-Subscription ist unvollständig")
        }

        const rows = await this.server.db
            .insert(notificationPushSubscriptions)
            .values({
                tenantId,
                userId,
                endpoint: subscription.endpoint,
                p256dh: subscription.keys.p256dh,
                auth: subscription.keys.auth,
                userAgent,
                deviceLabel: subscription.deviceLabel,
                meta: subscription.meta ?? null,
                lastSeenAt: new Date(),
                disabledAt: null,
            })
            .onConflictDoUpdate({
                target: notificationPushSubscriptions.endpoint,
                set: {
                    tenantId,
                    userId,
                    p256dh: subscription.keys.p256dh,
                    auth: subscription.keys.auth,
                    userAgent,
                    deviceLabel: subscription.deviceLabel,
                    meta: subscription.meta ?? null,
                    lastSeenAt: new Date(),
                    disabledAt: null,
                },
            })
            .returning()

        return rows[0]
    }

    async disablePushSubscription(tenantId: number, userId: string, endpoint: string) {
        await this.server.db
            .update(notificationPushSubscriptions)
            .set({ disabledAt: new Date() })
            .where(and(
                eq(notificationPushSubscriptions.tenantId, tenantId),
                eq(notificationPushSubscriptions.userId, userId),
                eq(notificationPushSubscriptions.endpoint, endpoint)
            ))

        return { success: true }
    }

    getPublicPushConfig() {
        return {
            configured: Boolean(secrets.WEB_PUSH_PUBLIC_KEY && secrets.WEB_PUSH_PRIVATE_KEY),
            publicKey: secrets.WEB_PUSH_PUBLIC_KEY || "",
        }
    }

    private async getActiveEventType(eventType: string) {
        const rows = await this.server.db
            .select()
            .from(notificationsEventTypes)
            .where(eq(notificationsEventTypes.eventKey, eventType))
            .limit(1)

        const row = rows[0]
        if (!row || row.isActive !== true) {
            throw new Error(`Unbekannter oder inaktiver Event-Typ: ${eventType}`)
        }

        return row
    }

    private normalizeChannels(value: unknown): NotificationChannel[] {
        if (!Array.isArray(value)) return DEFAULT_CHANNELS

        const valid = new Set(["inapp", "email", "push", "webhook", "sms"])
        const channels = value.filter((channel): channel is NotificationChannel =>
            typeof channel === "string" && valid.has(channel)
        )

        return channels.length ? channels : DEFAULT_CHANNELS
    }

    private async resolveEnabledChannels(input: {
        tenantId: number
        userId: string
        eventType: string
        channels: NotificationChannel[]
    }) {
        const prefs = await this.server.db
            .select()
            .from(notificationsPreferences)
            .where(and(
                eq(notificationsPreferences.tenantId, input.tenantId),
                eq(notificationsPreferences.userId, input.userId),
                eq(notificationsPreferences.eventType, input.eventType),
                inArray(notificationsPreferences.channel, input.channels)
            ))

        const defaults = await this.server.db
            .select()
            .from(notificationsPreferencesDefaults)
            .where(and(
                eq(notificationsPreferencesDefaults.tenantId, input.tenantId),
                eq(notificationsPreferencesDefaults.eventKey, input.eventType),
                inArray(notificationsPreferencesDefaults.channel, input.channels)
            ))

        return input.channels.filter((channel) => {
            const userPref = prefs.find((pref) => pref.channel === channel)
            if (userPref) return userPref.enabled

            const defaultPref = defaults.find((pref) => pref.channel === channel)
            if (defaultPref) return defaultPref.enabled

            return true
        })
    }

    private async deliver(item: typeof notificationsItems.$inferSelect) {
        if (item.channel === "inapp") {
            await this.markSent(item.id)
            return { success: true, id: item.id, channel: item.channel }
        }

        if (item.channel === "push") {
            return await this.deliverPush(item)
        }

        if (item.channel === "email") {
            return await this.deliverEmail(item)
        }

        await this.markFailed(item.id, `Kein Zusteller für Kanal ${item.channel}`)
        return { success: false, id: item.id, channel: item.channel }
    }

    private async deliverPush(item: typeof notificationsItems.$inferSelect) {
        const subscriptions = secrets.WEB_PUSH_PUBLIC_KEY && secrets.WEB_PUSH_PRIVATE_KEY
            ? await this.server.db
            .select()
            .from(notificationPushSubscriptions)
            .where(and(
                eq(notificationPushSubscriptions.tenantId, item.tenantId),
                eq(notificationPushSubscriptions.userId, item.userId),
                isNull(notificationPushSubscriptions.disabledAt)
            ))
            : []

        const mobileDevices = await this.server.db
            .select({ centralDeviceId: notificationMobilePushDevices.centralDeviceId })
            .from(notificationMobilePushDevices)
            .where(and(
                eq(notificationMobilePushDevices.tenantId, item.tenantId),
                eq(notificationMobilePushDevices.userId, item.userId),
                isNull(notificationMobilePushDevices.disabledAt)
            ))

        if (!subscriptions.length && !mobileDevices.length) {
            await this.markFailed(item.id, "Keine aktive Push-Subscription")
            return { success: false, id: item.id, channel: item.channel }
        }

        const payload = JSON.stringify({
            id: item.id,
            title: item.title,
            message: item.message,
            payload: item.payload || {},
        })

        let delivered = 0
        const errors: string[] = []

        if (mobileDevices.length) {
            try {
                const result = await pushServerClient.sendPush({
                    idempotencyKey: `notification:${item.id}`,
                    devices: mobileDevices.map((device) => device.centralDeviceId),
                    priority: "high",
                    ttlSeconds: 3600,
                    notification: {
                        title: item.title,
                        body: item.message,
                    },
                    data: {
                        notificationId: item.id,
                        ...(typeof item.payload === "object" && item.payload !== null ? item.payload as Record<string, unknown> : {}),
                    },
                })
                delivered += result.accepted
                if (result.rejected) errors.push(`${result.rejected} mobile Geräte vom Push-Server abgelehnt`)
            } catch (error: any) {
                errors.push(error?.message || String(error))
            }
        }

        if (subscriptions.length) {
            webPush.setVapidDetails(
                secrets.WEB_PUSH_SUBJECT || "mailto:admin@example.com",
                secrets.WEB_PUSH_PUBLIC_KEY!,
                secrets.WEB_PUSH_PRIVATE_KEY!
            )
        }

        for (const subscription of subscriptions) {
            try {
                await webPush.sendNotification({
                    endpoint: subscription.endpoint,
                    keys: {
                        p256dh: subscription.p256dh,
                        auth: subscription.auth,
                    },
                }, payload)

                delivered++
                await this.server.db
                    .update(notificationPushSubscriptions)
                    .set({ lastSeenAt: new Date() })
                    .where(eq(notificationPushSubscriptions.id, subscription.id))
            } catch (error: any) {
                errors.push(error?.message || String(error))

                if (error?.statusCode === 404 || error?.statusCode === 410) {
                    await this.server.db
                        .update(notificationPushSubscriptions)
                        .set({ disabledAt: new Date() })
                        .where(eq(notificationPushSubscriptions.id, subscription.id))
                }
            }
        }

        if (delivered > 0) {
            await this.markSent(item.id)
            return { success: true, id: item.id, channel: item.channel, delivered }
        }

        await this.markFailed(item.id, errors.join("; ") || "Push konnte nicht zugestellt werden")
        return { success: false, id: item.id, channel: item.channel }
    }

    private async deliverEmail(item: typeof notificationsItems.$inferSelect) {
        try {
            const user = await this.getUser(this.server, item.userId, item.tenantId)
            if (!user?.email) throw new Error(`Nutzer ${item.userId} hat keine E-Mail-Adresse`)

            await this.sendEmail(user.email, item.title, item.message)
            await this.markSent(item.id)
            return { success: true, id: item.id, channel: item.channel }
        } catch (error: any) {
            await this.markFailed(item.id, error?.message || "E-Mail Versand fehlgeschlagen")
            this.server.log.error({ err: error, notificationId: item.id }, "E-Mail Versand fehlgeschlagen")
            return { success: false, id: item.id, channel: item.channel }
        }
    }

    private async markSent(id: string) {
        await this.server.db
            .update(notificationsItems)
            .set({ status: "sent", sentAt: new Date() })
            .where(eq(notificationsItems.id, id))
    }

    private async markFailed(id: string, error: string) {
        await this.server.db
            .update(notificationsItems)
            .set({ status: "failed", error })
            .where(eq(notificationsItems.id, id))
    }

    private async sendEmail(to: string, subject: string, message: string) {
        const nodemailer = await import("nodemailer")

        const transporter = nodemailer.createTransport({
            host: secrets.MAILER_SMTP_HOST,
            port: Number(secrets.MAILER_SMTP_PORT),
            secure: secrets.MAILER_SMTP_SSL === "true",
            auth: {
                user: secrets.MAILER_SMTP_USER,
                pass: secrets.MAILER_SMTP_PASS,
            },
        })

        const html = this.renderFedeoHtml(subject, message)

        await transporter.sendMail({
            from: secrets.MAILER_FROM,
            to,
            subject,
            text: message,
            html,
        })
    }

    private renderFedeoHtml(title: string, message: string) {
        return `
      <html><body style="font-family:sans-serif;color:#222">
        <div style="border:1px solid #ddd;border-radius:8px;padding:16px;max-width:600px;margin:auto">
          <h2 style="color:#0f62fe;margin:0 0 12px">FEDEO</h2>
          <h3 style="margin:0 0 8px">${this.escapeHtml(title)}</h3>
          <p>${this.nl2br(this.escapeHtml(message))}</p>
          <hr style="margin:16px 0;border:none;border-top:1px solid #eee"/>
          <p style="font-size:12px;color:#666">Automatisch generiert von FEDEO</p>
        </div>
      </body></html>
    `
    }

    private escapeHtml(s: string) {
        return s
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
    }

    private nl2br(s: string) {
        return s.replace(/\n/g, "<br/>")
    }
}
