import { FastifyInstance } from "fastify"
import { and, asc, desc, eq } from "drizzle-orm"
import { ImapFlow } from "imapflow"
import { simpleParser } from "mailparser"

import {
    emailAttachments,
    emailMailboxes,
    emailMessageBodies,
    emailMessages,
    emailSyncState,
    userCredentials,
} from "../../../db/schema"
import { decrypt } from "../../utils/crypt"

type EmailAddress = {
    name?: string | null
    address?: string | null
}

type SyncOptions = {
    mailbox?: string
    limit?: number
}

type MailAccountConnection = {
    id: string
    tenantId: number
    userId: string
    email: string
    password: string
    imapHost: string
    imapPort: number
    imapSsl: boolean
}

const decryptValue = (value: unknown) => value ? decrypt(value as any) : ""

const normalizeAddressList = (addresses: any): EmailAddress[] => {
    const value = Array.isArray(addresses?.value) ? addresses.value : []
    return value.map((item: any) => ({
        name: item.name || null,
        address: item.address || null,
    }))
}

const previewText = (text?: string | false | null) => {
    if (!text) return null
    return text.replace(/\s+/g, " ").trim().slice(0, 240) || null
}

const attachmentFilename = (node: any) =>
    node?.dispositionParameters?.filename
    || node?.parameters?.name
    || null

const normalizeContentId = (value?: string | null) =>
    value ? value.replace(/^<|>$/g, "") : null

const collectAttachmentParts = (node: any, parts: any[] = []) => {
    if (!node) return parts

    if (node.part && !node.childNodes?.length) {
        const filename = attachmentFilename(node)
        const disposition = String(node.disposition || "").toLowerCase()

        if (filename || node.id || ["attachment", "inline"].includes(disposition)) {
            parts.push(node)
        }
    }

    for (const child of node.childNodes || []) {
        collectAttachmentParts(child, parts)
    }

    return parts
}

const findAttachmentPart = (bodyStructure: any, attachment: any) => {
    const parts = collectAttachmentParts(bodyStructure)
    if (!parts.length) return null

    const scored = parts
        .map((part) => {
            let score = 0
            if (attachmentFilename(part) && attachmentFilename(part) === attachment.filename) score += 4
            if (part.type && part.type === attachment.contentType) score += 3
            if (Number(part.size || 0) === Number(attachment.size || 0)) score += 2
            if (
                normalizeContentId(part.id)
                && normalizeContentId(part.id) === normalizeContentId(attachment.contentId)
            ) {
                score += 3
            }
            return { part, score }
        })
        .sort((a, b) => b.score - a.score)

    if (scored[0]?.score > 0) return scored[0].part
    return parts.length === 1 ? parts[0] : null
}

const streamToBuffer = async (stream: any, timeoutMs = 45_000) => {
    const chunks: Buffer[] = []
    let timeout: NodeJS.Timeout | null = null

    try {
        await Promise.race([
            (async () => {
                for await (const chunk of stream) {
                    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
                }
            })(),
            new Promise((_, reject) => {
                timeout = setTimeout(() => {
                    if (typeof stream.destroy === "function") {
                        stream.destroy()
                    }
                    reject(new Error("Anhang-Download hat zu lange gedauert"))
                }, timeoutMs)
            }),
        ])
    } finally {
        if (timeout) clearTimeout(timeout)
    }

    return Buffer.concat(chunks)
}

const flagsFromMessage = (flags: Set<string> | string[] | undefined) => {
    if (!flags) return []
    return Array.isArray(flags) ? flags : Array.from(flags)
}

const mailboxDisplayName = (path: string) => {
    const parts = path.split(/[/.]/).filter(Boolean)
    return parts[parts.length - 1] || path
}

export function emailSyncService(server: FastifyInstance) {
    const getAccount = async (
        tenantId: number,
        userId: string,
        accountId: string,
    ): Promise<MailAccountConnection | null> => {
        const rows = await server.db
            .select()
            .from(userCredentials)
            .where(and(
                eq(userCredentials.id, accountId),
                eq(userCredentials.tenantId, tenantId),
                eq(userCredentials.userId, userId),
                eq(userCredentials.type, "mail"),
            ))
            .limit(1)

        const row = rows[0]
        if (!row) return null

        return {
            id: row.id,
            tenantId: row.tenantId,
            userId: row.userId,
            email: decryptValue(row.emailEncrypted),
            password: decryptValue(row.passwordEncrypted),
            imapHost: decryptValue(row.imapHostEncrypted),
            imapPort: Number(row.imapPort || 993),
            imapSsl: row.imapSsl !== false,
        }
    }

    const createClient = (account: MailAccountConnection) => new ImapFlow({
        host: account.imapHost,
        port: account.imapPort,
        secure: account.imapSsl,
        auth: {
            user: account.email,
            pass: account.password,
        },
        logger: false,
    })

    const upsertMailbox = async (
        account: MailAccountConnection,
        mailbox: any,
        status?: { exists?: number; unseen?: number },
    ) => {
        const path = mailbox.path || mailbox.name
        const [saved] = await server.db
            .insert(emailMailboxes)
            .values({
                tenantId: account.tenantId,
                userId: account.userId,
                accountId: account.id,
                path,
                delimiter: mailbox.delimiter || null,
                name: mailbox.name || mailboxDisplayName(path),
                specialUse: mailbox.specialUse || null,
                flags: flagsFromMessage(mailbox.flags),
                exists: status?.exists || 0,
                unseen: status?.unseen || 0,
                updatedAt: new Date(),
            })
            .onConflictDoUpdate({
                target: [emailMailboxes.accountId, emailMailboxes.path],
                set: {
                    delimiter: mailbox.delimiter || null,
                    name: mailbox.name || mailboxDisplayName(path),
                    specialUse: mailbox.specialUse || null,
                    flags: flagsFromMessage(mailbox.flags),
                    exists: status?.exists || 0,
                    unseen: status?.unseen || 0,
                    updatedAt: new Date(),
                },
            })
            .returning()

        return saved
    }

    const syncMailboxes = async (account: MailAccountConnection, client: ImapFlow) => {
        const savedMailboxes = []

        for await (const mailbox of await client.list()) {
            savedMailboxes.push(await upsertMailbox(account, mailbox))
        }

        return savedMailboxes
    }

    const loadSyncState = async (account: MailAccountConnection, mailbox: any) => {
        const rows = await server.db
            .select()
            .from(emailSyncState)
            .where(and(
                eq(emailSyncState.accountId, account.id),
                eq(emailSyncState.mailboxPath, mailbox.path),
            ))
            .limit(1)

        return rows[0] || null
    }

    const saveSyncState = async (
        account: MailAccountConnection,
        mailbox: any,
        highestUid: number,
        uidValidity?: number | null,
        syncError?: string | null,
    ) => {
        await server.db
            .insert(emailSyncState)
            .values({
                tenantId: account.tenantId,
                userId: account.userId,
                accountId: account.id,
                mailboxId: mailbox.id,
                mailboxPath: mailbox.path,
                uidValidity: uidValidity || null,
                highestUid,
                lastSyncedAt: new Date(),
                syncError: syncError || null,
                updatedAt: new Date(),
            })
            .onConflictDoUpdate({
                target: [emailSyncState.accountId, emailSyncState.mailboxPath],
                set: {
                    mailboxId: mailbox.id,
                    uidValidity: uidValidity || null,
                    highestUid,
                    lastSyncedAt: new Date(),
                    syncError: syncError || null,
                    updatedAt: new Date(),
                },
            })
    }

    const storeMessage = async (
        account: MailAccountConnection,
        mailbox: any,
        message: any,
    ) => {
        if (!message.source) return null

        const parsed = await simpleParser(message.source)
        const flags = flagsFromMessage(message.flags)
        const receivedAt = parsed.date || message.envelope?.date || new Date()
        const threadId = parsed.inReplyTo || parsed.references?.[0] || parsed.messageId || message.emailId || null

        const [saved] = await server.db
            .insert(emailMessages)
            .values({
                tenantId: account.tenantId,
                userId: account.userId,
                accountId: account.id,
                mailboxId: mailbox.id,
                mailboxPath: mailbox.path,
                uid: Number(message.uid),
                emailId: message.emailId || null,
                messageId: parsed.messageId || null,
                inReplyTo: parsed.inReplyTo || null,
                threadId,
                subject: parsed.subject || "(kein Betreff)",
                from: normalizeAddressList(parsed.from),
                to: normalizeAddressList(parsed.to),
                cc: normalizeAddressList(parsed.cc),
                bcc: normalizeAddressList(parsed.bcc),
                replyTo: normalizeAddressList(parsed.replyTo),
                preview: previewText(parsed.text),
                flags,
                seen: flags.includes("\\Seen"),
                flagged: flags.includes("\\Flagged"),
                hasAttachments: Boolean(parsed.attachments?.length),
                size: message.size || null,
                sentAt: parsed.date || null,
                receivedAt,
                updatedAt: new Date(),
            })
            .onConflictDoUpdate({
                target: [emailMessages.mailboxId, emailMessages.uid],
                set: {
                    emailId: message.emailId || null,
                    messageId: parsed.messageId || null,
                    inReplyTo: parsed.inReplyTo || null,
                    threadId,
                    subject: parsed.subject || "(kein Betreff)",
                    from: normalizeAddressList(parsed.from),
                    to: normalizeAddressList(parsed.to),
                    cc: normalizeAddressList(parsed.cc),
                    bcc: normalizeAddressList(parsed.bcc),
                    replyTo: normalizeAddressList(parsed.replyTo),
                    preview: previewText(parsed.text),
                    flags,
                    seen: flags.includes("\\Seen"),
                    flagged: flags.includes("\\Flagged"),
                    hasAttachments: Boolean(parsed.attachments?.length),
                    size: message.size || null,
                    sentAt: parsed.date || null,
                    receivedAt,
                    updatedAt: new Date(),
                },
            })
            .returning()

        await server.db
            .insert(emailMessageBodies)
            .values({
                messageId: saved.id,
                text: parsed.text || null,
                html: parsed.html || null,
                updatedAt: new Date(),
            })
            .onConflictDoUpdate({
                target: emailMessageBodies.messageId,
                set: {
                    text: parsed.text || null,
                    html: parsed.html || null,
                    updatedAt: new Date(),
                },
            })

        if (parsed.attachments?.length) {
            for (const attachment of parsed.attachments) {
                await server.db
                    .insert(emailAttachments)
                    .values({
                        messageId: saved.id,
                        filename: attachment.filename || null,
                        contentType: attachment.contentType || null,
                        contentId: attachment.contentId || null,
                        disposition: attachment.contentDisposition || null,
                        size: attachment.size || null,
                        checksum: attachment.checksum || null,
                    })
                    .onConflictDoNothing()
            }
        }

        return saved
    }

    const updateCachedMessageFlags = async (
        mailboxId: string,
        uid: number,
        flags: string[],
    ) => {
        await server.db
            .update(emailMessages)
            .set({
                flags,
                seen: flags.includes("\\Seen"),
                flagged: flags.includes("\\Flagged"),
                updatedAt: new Date(),
            })
            .where(and(
                eq(emailMessages.mailboxId, mailboxId),
                eq(emailMessages.uid, uid),
            ))
    }

    const syncMailboxMessages = async (
        account: MailAccountConnection,
        client: ImapFlow,
        mailbox: any,
        limit: number,
    ) => {
        const lock = await client.getMailboxLock(mailbox.path)
        let highestUid = 0

        try {
            const opened: any = await client.mailboxOpen(mailbox.path)
            await upsertMailbox(account, mailbox, {
                exists: opened.exists || 0,
                unseen: opened.unseen || 0,
            })

            const state = await loadSyncState(account, mailbox)
            const searchResult = await client.search({ all: true }, { uid: true })
            const allUids = Array.isArray(searchResult) ? searchResult : []
            const newUids = allUids
                .filter((uid: number) => !state?.highestUid || uid > state.highestUid)
                .slice(-limit)
            const flagSyncUids = allUids.slice(-limit)

            highestUid = Math.max(state?.highestUid || 0, ...newUids, 0)

            if (flagSyncUids.length) {
                for await (const message of client.fetch(flagSyncUids, {
                    uid: true,
                    flags: true,
                }, { uid: true })) {
                    await updateCachedMessageFlags(
                        mailbox.id,
                        Number(message.uid),
                        flagsFromMessage(message.flags),
                    )
                }
            }

            if (newUids.length) {
                for await (const message of client.fetch(newUids, {
                    uid: true,
                    envelope: true,
                    flags: true,
                    source: true,
                    size: true,
                }, { uid: true })) {
                    await storeMessage(account, mailbox, message)
                }
            }

            await saveSyncState(account, mailbox, highestUid, Number(opened.uidValidity || 0))
            return { path: mailbox.path, fetched: newUids.length, highestUid }
        } catch (err: any) {
            await saveSyncState(account, mailbox, highestUid, null, err.message || "Sync fehlgeschlagen")
            throw err
        } finally {
            lock.release()
        }
    }

    const syncAccount = async (
        tenantId: number,
        userId: string,
        accountId: string,
        options: SyncOptions = {},
    ) => {
        const account = await getAccount(tenantId, userId, accountId)
        if (!account) {
            throw new Error("E-Mail Konto nicht gefunden")
        }

        const client = createClient(account)
        const limit = Math.min(Math.max(Number(options.limit || 50), 1), 200)

        await client.connect()

        try {
            const mailboxes = await syncMailboxes(account, client)
            const syncTargets = options.mailbox
                ? mailboxes.filter((mailbox) => mailbox.path === options.mailbox)
                : mailboxes.filter((mailbox) => mailbox.specialUse === "\\Inbox" || mailbox.path.toUpperCase() === "INBOX")

            const synced = []
            for (const mailbox of syncTargets.length ? syncTargets : mailboxes.slice(0, 1)) {
                synced.push(await syncMailboxMessages(account, client, mailbox, limit))
            }

            return {
                accountId,
                mailboxes: mailboxes.length,
                synced,
            }
        } finally {
            await client.logout().catch(() => client.close())
        }
    }

    const setMessageSeen = async (
        tenantId: number,
        userId: string,
        messageId: string,
        seen: boolean,
    ) => {
        const rows = await server.db
            .select()
            .from(emailMessages)
            .where(and(
                eq(emailMessages.tenantId, tenantId),
                eq(emailMessages.userId, userId),
                eq(emailMessages.id, messageId),
            ))
            .limit(1)

        const message = rows[0]
        if (!message) return null

        const account = await getAccount(tenantId, userId, message.accountId)
        if (!account) {
            throw new Error("E-Mail Konto nicht gefunden")
        }

        const client = createClient(account)
        await client.connect()

        try {
            const lock = await client.getMailboxLock(message.mailboxPath)
            try {
                await client.mailboxOpen(message.mailboxPath)
                if (seen) {
                    await client.messageFlagsAdd(message.uid, ["\\Seen"], { uid: true })
                } else {
                    await client.messageFlagsRemove(message.uid, ["\\Seen"], { uid: true })
                }
            } finally {
                lock.release()
            }
        } finally {
            await client.logout().catch(() => client.close())
        }

        const currentFlags = Array.isArray(message.flags) ? message.flags : []
        const nextFlags = seen
            ? Array.from(new Set([...currentFlags, "\\Seen"]))
            : currentFlags.filter((flag) => flag !== "\\Seen")

        const [updated] = await server.db
            .update(emailMessages)
            .set({
                flags: nextFlags,
                seen,
                updatedAt: new Date(),
            })
            .where(eq(emailMessages.id, messageId))
            .returning()

        return updated
    }

    const loadMessageForAction = async (tenantId: number, userId: string, messageId: string) => {
        const rows = await server.db
            .select()
            .from(emailMessages)
            .where(and(
                eq(emailMessages.tenantId, tenantId),
                eq(emailMessages.userId, userId),
                eq(emailMessages.id, messageId),
            ))
            .limit(1)

        return rows[0] || null
    }

    const deleteCachedMessage = async (messageId: string) => {
        await server.db
            .delete(emailMessages)
            .where(eq(emailMessages.id, messageId))
    }

    const moveMessage = async (
        tenantId: number,
        userId: string,
        messageId: string,
        destinationMailboxPath: string,
    ) => {
        const message = await loadMessageForAction(tenantId, userId, messageId)
        if (!message) return null

        if (message.mailboxPath === destinationMailboxPath) {
            return message
        }

        const account = await getAccount(tenantId, userId, message.accountId)
        if (!account) {
            throw new Error("E-Mail Konto nicht gefunden")
        }

        const targetRows = await server.db
            .select({ path: emailMailboxes.path })
            .from(emailMailboxes)
            .where(and(
                eq(emailMailboxes.tenantId, tenantId),
                eq(emailMailboxes.userId, userId),
                eq(emailMailboxes.accountId, message.accountId),
                eq(emailMailboxes.path, destinationMailboxPath),
            ))
            .limit(1)

        if (!targetRows[0]) {
            throw new Error("Zielordner nicht gefunden")
        }

        const client = createClient(account)
        await client.connect()

        try {
            const lock = await client.getMailboxLock(message.mailboxPath)
            try {
                await client.mailboxOpen(message.mailboxPath)
                await client.messageMove(message.uid, destinationMailboxPath, { uid: true })
            } finally {
                lock.release()
            }
        } finally {
            await client.logout().catch(() => client.close())
        }

        await deleteCachedMessage(messageId)
        return { success: true, destinationMailboxPath }
    }

    const archiveMessage = async (tenantId: number, userId: string, messageId: string) => {
        const message = await loadMessageForAction(tenantId, userId, messageId)
        if (!message) return null

        const mailboxes = await server.db
            .select()
            .from(emailMailboxes)
            .where(and(
                eq(emailMailboxes.tenantId, tenantId),
                eq(emailMailboxes.userId, userId),
                eq(emailMailboxes.accountId, message.accountId),
            ))

        const archiveMailbox = mailboxes.find((mailbox) => mailbox.specialUse === "\\Archive")
            || mailboxes.find((mailbox) => ["archive", "archiv"].includes(mailbox.name.toLowerCase()))
            || mailboxes.find((mailbox) => ["archive", "archiv"].includes(mailbox.path.toLowerCase()))

        if (!archiveMailbox) {
            throw new Error("Kein Archivordner gefunden")
        }

        return await moveMessage(tenantId, userId, messageId, archiveMailbox.path)
    }

    const deleteMessage = async (tenantId: number, userId: string, messageId: string) => {
        const message = await loadMessageForAction(tenantId, userId, messageId)
        if (!message) return null

        const account = await getAccount(tenantId, userId, message.accountId)
        if (!account) {
            throw new Error("E-Mail Konto nicht gefunden")
        }

        const client = createClient(account)
        await client.connect()

        try {
            const lock = await client.getMailboxLock(message.mailboxPath)
            try {
                await client.mailboxOpen(message.mailboxPath)
                await client.messageDelete(message.uid, { uid: true })
            } finally {
                lock.release()
            }
        } finally {
            await client.logout().catch(() => client.close())
        }

        await deleteCachedMessage(messageId)
        return { success: true }
    }

    const getAttachmentContent = async (
        tenantId: number,
        userId: string,
        attachmentId: string,
    ) => {
        const rows = await server.db
            .select({
                attachment: emailAttachments,
                message: emailMessages,
            })
            .from(emailAttachments)
            .innerJoin(emailMessages, eq(emailMessages.id, emailAttachments.messageId))
            .where(and(
                eq(emailAttachments.id, attachmentId),
                eq(emailMessages.tenantId, tenantId),
                eq(emailMessages.userId, userId),
            ))
            .limit(1)

        const row = rows[0]
        if (!row) return null

        const messageAttachments = await server.db
            .select()
            .from(emailAttachments)
            .where(eq(emailAttachments.messageId, row.message.id))
            .orderBy(asc(emailAttachments.createdAt), asc(emailAttachments.id))
        const attachmentIndex = messageAttachments.findIndex((attachment) => attachment.id === attachmentId)

        const account = await getAccount(tenantId, userId, row.message.accountId)
        if (!account) {
            throw new Error("E-Mail Konto nicht gefunden")
        }

        const client = createClient(account)
        await client.connect()

        try {
            const lock = await client.getMailboxLock(row.message.mailboxPath)
            try {
                await client.mailboxOpen(row.message.mailboxPath)

                const structureMessage = await client.fetchOne(String(row.message.uid), {
                    uid: true,
                    bodyStructure: true,
                }, { uid: true })
                const attachmentPart = structureMessage
                    ? findAttachmentPart(structureMessage.bodyStructure, row.attachment)
                    : null

                if (attachmentPart?.part) {
                    const downloaded = await client.download(String(row.message.uid), attachmentPart.part, { uid: true })
                    const content = await streamToBuffer(downloaded.content)

                    if (content.length) {
                        return {
                            filename: downloaded.meta?.filename
                                || attachmentFilename(attachmentPart)
                                || row.attachment.filename
                                || "anhang",
                            contentType: downloaded.meta?.contentType
                                || attachmentPart.type
                                || row.attachment.contentType
                                || "application/octet-stream",
                            content,
                        }
                    }
                }

                const fetched = await client.fetchOne(String(row.message.uid), {
                    source: true,
                }, { uid: true })
                if (!fetched || !fetched.source) return null

                const parsed = await simpleParser(fetched.source)
                const matchedAttachment = parsed.attachments.find((item) =>
                    (row.attachment.checksum && item.checksum === row.attachment.checksum)
                    || (
                        item.filename === row.attachment.filename
                        && item.contentType === row.attachment.contentType
                        && Number(item.size || 0) === Number(row.attachment.size || 0)
                    )
                )
                const attachment = matchedAttachment
                    || parsed.attachments[attachmentIndex]
                    || (parsed.attachments.length === 1 ? parsed.attachments[0] : null)

                if (!attachment) {
                    return null
                }

                return {
                    filename: attachment.filename || row.attachment.filename || "anhang",
                    contentType: attachment.contentType || row.attachment.contentType || "application/octet-stream",
                    content: Buffer.isBuffer(attachment.content)
                        ? attachment.content
                        : Buffer.from(attachment.content),
                }
            } finally {
                lock.release()
            }
        } finally {
            await client.logout().catch(() => client.close())
        }

        return null
    }

    const listMailboxes = async (tenantId: number, userId: string, accountId: string) => {
        return await server.db
            .select()
            .from(emailMailboxes)
            .where(and(
                eq(emailMailboxes.tenantId, tenantId),
                eq(emailMailboxes.userId, userId),
                eq(emailMailboxes.accountId, accountId),
            ))
            .orderBy(emailMailboxes.specialUse, emailMailboxes.name)
    }

    const listMessages = async (
        tenantId: number,
        userId: string,
        accountId: string,
        mailboxPath = "INBOX",
        limit = 50,
    ) => {
        return await server.db
            .select()
            .from(emailMessages)
            .where(and(
                eq(emailMessages.tenantId, tenantId),
                eq(emailMessages.userId, userId),
                eq(emailMessages.accountId, accountId),
                eq(emailMessages.mailboxPath, mailboxPath),
            ))
            .orderBy(desc(emailMessages.receivedAt))
            .limit(Math.min(Math.max(Number(limit), 1), 200))
    }

    const getMessage = async (tenantId: number, userId: string, messageId: string) => {
        const rows = await server.db
            .select({
                message: emailMessages,
                body: emailMessageBodies,
            })
            .from(emailMessages)
            .leftJoin(emailMessageBodies, eq(emailMessageBodies.messageId, emailMessages.id))
            .where(and(
                eq(emailMessages.tenantId, tenantId),
                eq(emailMessages.userId, userId),
                eq(emailMessages.id, messageId),
            ))
            .limit(1)

        if (!rows[0]) return null

        const attachments = await server.db
            .select()
            .from(emailAttachments)
            .where(eq(emailAttachments.messageId, messageId))

        return {
            ...rows[0].message,
            body: rows[0].body,
            attachments,
        }
    }

    return {
        syncAccount,
        listMailboxes,
        listMessages,
        getMessage,
        setMessageSeen,
        moveMessage,
        archiveMessage,
        deleteMessage,
        getAttachmentContent,
    }
}
