import { FastifyInstance } from "fastify"
import axios from "axios"
import dayjs from "dayjs"

import { secrets } from "../utils/secrets"
import { insertHistoryItem } from "../utils/history"
import { decrypt, encrypt } from "../utils/crypt"
import { DE_BANK_CODE_TO_NAME } from "../utils/deBankCodes"
import { DE_BANK_CODE_TO_BIC } from "../utils/deBankBics"

import {
    bankrequisitions,
    bankaccounts,
    bankstatements,
    accounts,
    createddocuments,
    customers,
    entitybankaccounts,
    incominginvoices,
    ownaccounts,
    statementallocations,
    vendors,
} from "../../db/schema"

import {
    eq,
    and,
    isNull,
    aliasedTable,
    desc,
} from "drizzle-orm"


export default async function bankingRoutes(server: FastifyInstance) {
    const CASHBOOK_BANK_ID = "fedeo-cashbook"
    const ContraAccounts = aliasedTable(accounts, "contra_accounts")
    const ContraCustomers = aliasedTable(customers, "contra_customers")
    const ContraVendors = aliasedTable(vendors, "contra_vendors")
    const ContraOwnaccounts = aliasedTable(ownaccounts, "contra_ownaccounts")
    const ManualInvoices = aliasedTable(incominginvoices, "manual_invoices")
    const ManualInvoiceVendors = aliasedTable(vendors, "manual_invoice_vendors")

    const normalizeManualSide = (payload: any, keys: string[]) =>
        keys.filter((key) => payload[key] !== null && payload[key] !== undefined && payload[key] !== "")

    const cashbookAccountFilter = (tenantId: number) => and(
        eq(bankaccounts.tenant, tenantId),
        eq(bankaccounts.bankId, CASHBOOK_BANK_ID),
        eq(bankaccounts.archived, false)
    )

    const buildCashbookCounterPayload = (type: string, id: any) => {
        const numericId = type === "ownaccount" ? id : Number(id)
        if (!id || (type !== "ownaccount" && !Number.isFinite(numericId))) return null

        if (type === "account") return { account: numericId }
        if (type === "customer") return { customer: numericId }
        if (type === "vendor") return { vendor: numericId }
        if (type === "ownaccount") return { ownaccount: numericId }
        if (type === "incominginvoice") return { incominginvoice: numericId }
        return null
    }

    const prepareStatementAllocationPayload = (payload: any) => {
        const next = { ...payload }
        const isManualBooking = !next.bankstatement

        if (!isManualBooking) {
            next.manualBookingDate = null
            next.contraAccount = null
            next.contraCustomer = null
            next.contraVendor = null
            next.contraOwnaccount = null
            next.datevTaxKey = next.datevTaxKey ? String(next.datevTaxKey).trim() : null
            next.manualInvoiceSide = null
            return { data: next }
        }

        const debitKeys = ["account", "customer", "vendor", "ownaccount"]
        const creditKeys = ["contraAccount", "contraCustomer", "contraVendor", "contraOwnaccount"]
        const hasManualInvoice = next.incominginvoice !== null && next.incominginvoice !== undefined && next.incominginvoice !== ""
        const debitSide = normalizeManualSide(next, debitKeys)
        const creditSide = normalizeManualSide(next, creditKeys)

        if (hasManualInvoice) {
            if (next.manualInvoiceSide === "debit") debitSide.push("incominginvoice")
            else if (next.manualInvoiceSide === "credit") creditSide.push("incominginvoice")
            else return { error: "Für zugewiesene Eingangsbelege muss Soll oder Haben ausgewählt sein." }
        } else {
            next.manualInvoiceSide = null
        }

        if (!next.manualBookingDate || !dayjs(next.manualBookingDate).isValid()) {
            return { error: "Für manuelle Buchungen ist ein gültiges Buchungsdatum erforderlich." }
        }

        if (!Number.isFinite(Number(next.amount)) || Number(next.amount) <= 0) {
            return { error: "Für manuelle Buchungen muss der Betrag größer als 0 sein." }
        }

        if (debitSide.length !== 1 || creditSide.length !== 1) {
            return { error: "Für manuelle Buchungen muss genau ein Soll- und ein Haben-Konto ausgewählt werden." }
        }

        next.amount = Math.abs(Number(next.amount))
        next.bankstatement = null
        next.manualBookingDate = dayjs(next.manualBookingDate).format("YYYY-MM-DD")
        next.datevTaxKey = next.datevTaxKey ? String(next.datevTaxKey).trim() : null

        return { data: next }
    }

    server.get("/banking/cashbooks", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const rows = await server.db
                .select()
                .from(bankaccounts)
                .where(cashbookAccountFilter(req.user.tenant_id))
                .orderBy(bankaccounts.name)

            return reply.send(rows)
        } catch (err) {
            server.log.error(err)
            return reply.code(500).send({ error: "Failed to load cashbooks" })
        }
    })

    server.post("/banking/cashbooks", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const body = req.body as {
                name?: string
                datevNumber?: string
                openingBalance?: number
            }

            const name = String(body.name || "").trim()
            const datevNumber = String(body.datevNumber || "").trim()
            const openingBalance = Number(body.openingBalance || 0)

            if (!name) return reply.code(400).send({ error: "Bitte eine Bezeichnung für die Kasse angeben." })
            if (!datevNumber) return reply.code(400).send({ error: "Bitte eine Kontennummer für die Kasse angeben." })
            if (!Number.isFinite(openingBalance)) return reply.code(400).send({ error: "Der Anfangsbestand ist ungültig." })

            const uniquePart = `${req.user.tenant_id}-${Date.now()}`
            const inserted = await server.db.insert(bankaccounts).values({
                name,
                iban: `CASH-${uniquePart}`,
                tenant: req.user.tenant_id,
                bankId: CASHBOOK_BANK_ID,
                ownerName: name,
                accountId: `cashbook-${uniquePart}`,
                balance: openingBalance,
                datevNumber,
                updatedBy: req.user.user_id,
            }).returning()

            const createdRecord = inserted[0]
            return reply.send(createdRecord)
        } catch (err) {
            server.log.error(err)
            return reply.code(500).send({ error: "Failed to create cashbook" })
        }
    })

    server.patch("/banking/cashbooks/:id/archive", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const { id } = req.params as { id: string }
            const updated = await server.db.update(bankaccounts)
                .set({
                    archived: true,
                    updatedAt: new Date(),
                    updatedBy: req.user.user_id,
                })
                .where(and(
                    eq(bankaccounts.id, Number(id)),
                    eq(bankaccounts.tenant, req.user.tenant_id),
                    eq(bankaccounts.bankId, CASHBOOK_BANK_ID)
                ))
                .returning()

            if (!updated[0]) return reply.code(404).send({ error: "Cashbook not found" })
            return reply.send(updated[0])
        } catch (err) {
            server.log.error(err)
            return reply.code(500).send({ error: "Failed to archive cashbook" })
        }
    })

    server.get("/banking/cashbooks/:id/bookings", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const { id } = req.params as { id: string }
            const cashbookId = Number(id)
            if (!Number.isFinite(cashbookId)) return reply.code(400).send({ error: "Ungültige Kasse." })

            const rows = await server.db.select({
                statement: bankstatements,
                allocation: statementallocations,
                account: accounts,
                customer: customers,
                vendor: vendors,
                ownaccount: ownaccounts,
                incominginvoice: ManualInvoices,
                incominginvoiceVendor: ManualInvoiceVendors,
            })
                .from(bankstatements)
                .leftJoin(statementallocations, eq(statementallocations.bankstatement, bankstatements.id))
                .leftJoin(accounts, eq(statementallocations.account, accounts.id))
                .leftJoin(customers, eq(statementallocations.customer, customers.id))
                .leftJoin(vendors, eq(statementallocations.vendor, vendors.id))
                .leftJoin(ownaccounts, eq(statementallocations.ownaccount, ownaccounts.id))
                .leftJoin(ManualInvoices, eq(statementallocations.incominginvoice, ManualInvoices.id))
                .leftJoin(ManualInvoiceVendors, eq(ManualInvoices.vendor, ManualInvoiceVendors.id))
                .where(and(
                    eq(bankstatements.tenant, req.user.tenant_id),
                    eq(bankstatements.account, cashbookId),
                    eq(bankstatements.archived, false)
                ))
                .orderBy(desc(bankstatements.date), desc(bankstatements.createdAt))

            return reply.send(rows.map((row) => ({
                ...row.statement,
                allocation: row.allocation,
                account: row.account,
                customer: row.customer,
                vendor: row.vendor,
                ownaccount: row.ownaccount,
                incominginvoice: row.incominginvoice ? {
                    ...row.incominginvoice,
                    vendor: row.incominginvoiceVendor,
                } : null,
            })))
        } catch (err) {
            server.log.error(err)
            return reply.code(500).send({ error: "Failed to load cashbook bookings" })
        }
    })

    server.post("/banking/cashbooks/:id/bookings", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const { id } = req.params as { id: string }
            const cashbookId = Number(id)
            const body = req.body as {
                date?: string
                amount?: number
                direction?: "income" | "expense"
                counterType?: string
                counterId?: string | number
                description?: string
                datevTaxKey?: string | null
            }

            if (!Number.isFinite(cashbookId)) return reply.code(400).send({ error: "Ungültige Kasse." })
            const bookingDate = body.date && dayjs(body.date).isValid() ? dayjs(body.date) : dayjs()
            if (!Number.isFinite(Number(body.amount)) || Number(body.amount) <= 0) return reply.code(400).send({ error: "Der Betrag muss größer als 0 sein." })
            if (body.direction !== "income" && body.direction !== "expense") return reply.code(400).send({ error: "Bitte Einnahme oder Ausgabe auswählen." })

            const cashbook = await server.db.select().from(bankaccounts).where(and(
                eq(bankaccounts.id, cashbookId),
                eq(bankaccounts.tenant, req.user.tenant_id),
                eq(bankaccounts.bankId, CASHBOOK_BANK_ID),
                eq(bankaccounts.archived, false)
            )).limit(1)

            if (!cashbook[0]) return reply.code(404).send({ error: "Kasse nicht gefunden." })

            const hasCounterInput = Boolean(body.counterType || body.counterId)
            const counterPayload = hasCounterInput
                ? buildCashbookCounterPayload(String(body.counterType || ""), body.counterId)
                : null
            if (hasCounterInput && !counterPayload) return reply.code(400).send({ error: "Bitte ein gültiges Gegenkonto auswählen." })

            const signedAmount = body.direction === "income"
                ? Math.abs(Number(body.amount))
                : -Math.abs(Number(body.amount))
            const description = String(body.description || "").trim() || (body.direction === "income" ? "Bareinnahme" : "Barausgabe")

            const created = await server.db.transaction(async (tx) => {
                const insertedStatements = await tx.insert(bankstatements).values({
                    account: cashbookId,
                    date: bookingDate.format("YYYY-MM-DD"),
                    valueDate: bookingDate.format("YYYY-MM-DD"),
                    amount: signedAmount,
                    tenant: req.user.tenant_id,
                    text: description,
                    currency: "EUR",
                    credName: body.direction === "income" ? cashbook[0].name : description,
                    debName: body.direction === "expense" ? cashbook[0].name : description,
                    updatedBy: req.user.user_id,
                }).returning()

                const statement = insertedStatements[0]
                const insertedAllocations = counterPayload
                    ? await tx.insert(statementallocations).values({
                        bankstatement: statement.id,
                        amount: signedAmount,
                        tenant: req.user.tenant_id,
                        description,
                        datevTaxKey: body.datevTaxKey ? String(body.datevTaxKey).trim() : null,
                        ...counterPayload,
                    }).returning()
                    : []

                return {
                    statement,
                    allocation: insertedAllocations[0] || null,
                }
            })

            await insertHistoryItem(server, {
                entity: "bankstatements",
                entityId: Number(created.statement.id),
                action: "created",
                created_by: req.user.user_id,
                tenant_id: req.user.tenant_id,
                oldVal: null,
                newVal: created,
                text: "Kassenbuchung erstellt",
            })

            return reply.send(created)
        } catch (err) {
            server.log.error(err)
            return reply.code(500).send({ error: "Failed to create cashbook booking" })
        }
    })

    server.delete("/banking/cashbook-bookings/:id", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const { id } = req.params as { id: string }
            const statementId = Number(id)
            if (!Number.isFinite(statementId)) return reply.code(400).send({ error: "Ungültige Buchung." })

            const records = await server.db.select({
                statement: bankstatements,
                cashbook: bankaccounts,
            })
                .from(bankstatements)
                .innerJoin(bankaccounts, eq(bankstatements.account, bankaccounts.id))
                .where(and(
                    eq(bankstatements.id, statementId),
                    eq(bankstatements.tenant, req.user.tenant_id),
                    eq(bankaccounts.bankId, CASHBOOK_BANK_ID)
                ))
                .limit(1)

            if (!records[0]) return reply.code(404).send({ error: "Kassenbuchung nicht gefunden." })

            await server.db.transaction(async (tx) => {
                await tx.delete(statementallocations).where(eq(statementallocations.bankstatement, statementId))
                await tx.delete(bankstatements).where(eq(bankstatements.id, statementId))
            })

            await insertHistoryItem(server, {
                entity: "bankstatements",
                entityId: statementId,
                action: "deleted",
                created_by: req.user.user_id,
                tenant_id: req.user.tenant_id,
                oldVal: records[0].statement,
                newVal: null,
                text: "Kassenbuchung gelöscht",
            })

            return reply.send({ success: true })
        } catch (err) {
            server.log.error(err)
            return reply.code(500).send({ error: "Failed to delete cashbook booking" })
        }
    })

    const normalizeIban = (value?: string | null) =>
        String(value || "").replace(/\s+/g, "").toUpperCase()

    const normalizeName = (value?: string | null) =>
        String(value || "")
            .toLowerCase()
            .replace(/[^a-z0-9äöüß]+/gi, " ")
            .replace(/\s+/g, " ")
            .trim()

    const pickPartnerBankData = (statement: any, partnerType: "customer" | "vendor") => {
        if (!statement) return null

        const prefersDebit = partnerType === "customer"
            ? Number(statement.amount) >= 0
            : Number(statement.amount) > 0

        const primary = prefersDebit
            ? { iban: statement.debIban }
            : { iban: statement.credIban }
        const fallback = prefersDebit
            ? { iban: statement.credIban }
            : { iban: statement.debIban }

        const primaryIban = normalizeIban(primary.iban)
        if (primaryIban) {
            return {
                iban: primaryIban,
            }
        }

        const fallbackIban = normalizeIban(fallback.iban)
        if (fallbackIban) {
            return {
                iban: fallbackIban,
            }
        }

        return null
    }

    const pickPartnerReference = (statement: any, partnerType: "customer" | "vendor") => {
        if (!statement) return null

        const prefersDebit = partnerType === "customer"
            ? Number(statement.amount) >= 0
            : Number(statement.amount) > 0

        const primary = prefersDebit
            ? { iban: statement.debIban, name: statement.debName }
            : { iban: statement.credIban, name: statement.credName }
        const fallback = prefersDebit
            ? { iban: statement.credIban, name: statement.credName }
            : { iban: statement.debIban, name: statement.debName }

        return {
            iban: normalizeIban(primary.iban) || normalizeIban(fallback.iban) || null,
            name: String(primary.name || fallback.name || "").trim() || null,
        }
    }

    const mergePartnerIban = (infoData: Record<string, any>, iban: string, bankAccountId?: number | null) => {
        if (!iban && !bankAccountId) return infoData || {}
        const info = infoData && typeof infoData === "object" ? { ...infoData } : {}

        if (iban) {
            const existing = Array.isArray(info.bankingIbans) ? info.bankingIbans : []
            const merged = [...new Set([...existing.map((i: string) => normalizeIban(i)), iban])]
            info.bankingIbans = merged
            if (!info.bankingIban) info.bankingIban = iban
        }

        if (bankAccountId) {
            const existingIds = Array.isArray(info.bankAccountIds) ? info.bankAccountIds : []
            if (!existingIds.includes(bankAccountId)) {
                info.bankAccountIds = [...existingIds, bankAccountId]
            }
        }

        return info
    }

    const ibanLengthByCountry: Record<string, number> = {
        DE: 22,
        AT: 20,
        CH: 21,
        NL: 18,
        BE: 16,
        FR: 27,
        ES: 24,
        IT: 27,
        LU: 20,
    }

    const isValidIbanLocal = (iban: string) => {
        const normalized = normalizeIban(iban)
        if (!normalized || normalized.length < 15 || normalized.length > 34) return false
        if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(normalized)) return false

        const country = normalized.slice(0, 2)
        const expectedLength = ibanLengthByCountry[country]
        if (expectedLength && normalized.length !== expectedLength) return false

        const rearranged = normalized.slice(4) + normalized.slice(0, 4)
        let numeric = ""
        for (const ch of rearranged) {
            if (ch >= "A" && ch <= "Z") numeric += (ch.charCodeAt(0) - 55).toString()
            else numeric += ch
        }

        let remainder = 0
        for (const digit of numeric) {
            remainder = (remainder * 10 + Number(digit)) % 97
        }

        return remainder === 1
    }

    const resolveGermanBankDataFromIbanLocal = (iban: string) => {
        const normalized = normalizeIban(iban)
        if (!isValidIbanLocal(normalized)) return null

        // Für DE-IBANs kann die BLZ aus Position 5-12 lokal gelesen werden.
        if (normalized.startsWith("DE") && normalized.length === 22) {
            const bankCode = normalized.slice(4, 12)
            const bankName = DE_BANK_CODE_TO_NAME[bankCode] || `Unbekannt (BLZ ${bankCode})`
            const bic = DE_BANK_CODE_TO_BIC[bankCode] || null
            return {
                bankName,
                bic,
                bankCode,
            }
        }

        return null
    }

    const resolveEntityBankAccountId = async (
        tenantId: number,
        userId: string,
        iban: string
    ) => {
        const normalizedIban = normalizeIban(iban)
        if (!normalizedIban) return null

        const bankData = resolveGermanBankDataFromIbanLocal(normalizedIban)

        const allAccounts = await server.db
            .select({
                id: entitybankaccounts.id,
                ibanEncrypted: entitybankaccounts.ibanEncrypted,
                bankNameEncrypted: entitybankaccounts.bankNameEncrypted,
                bicEncrypted: entitybankaccounts.bicEncrypted,
            })
            .from(entitybankaccounts)
            .where(eq(entitybankaccounts.tenant, tenantId))

        const existing = allAccounts.find((row) => {
            if (!row.ibanEncrypted) return false
            try {
                const decryptedIban = decrypt(row.ibanEncrypted as any)
                return normalizeIban(decryptedIban) === normalizedIban
            } catch {
                return false
            }
        })

        if (existing?.id) {
            if (bankData) {
                let currentBankName = ""
                let currentBic = ""
                try {
                    currentBankName = String(decrypt(existing.bankNameEncrypted as any) || "").trim()
                } catch {
                    currentBankName = ""
                }
                try {
                    currentBic = String(decrypt((existing as any).bicEncrypted as any) || "").trim()
                } catch {
                    currentBic = ""
                }

                const nextBankName = bankData?.bankName || "Unbekannt"
                const nextBic = bankData?.bic || "UNBEKANNT"
                if (currentBankName !== nextBankName || currentBic !== nextBic) {
                    await server.db
                        .update(entitybankaccounts)
                        .set({
                            bankNameEncrypted: encrypt(nextBankName),
                            bicEncrypted: encrypt(nextBic),
                            updatedAt: new Date(),
                            updatedBy: userId,
                        })
                        .where(and(eq(entitybankaccounts.id, Number(existing.id)), eq(entitybankaccounts.tenant, tenantId)))
                }
            }

            return Number(existing.id)
        }

        const [created] = await server.db
            .insert(entitybankaccounts)
            .values({
                tenant: tenantId,
                ibanEncrypted: encrypt(normalizedIban),
                bicEncrypted: encrypt(bankData?.bic || "UNBEKANNT"),
                bankNameEncrypted: encrypt(bankData?.bankName || "Unbekannt"),
                description: "Automatisch aus Bankbuchung übernommen",
                updatedAt: new Date(),
                updatedBy: userId,
            })
            .returning({ id: entitybankaccounts.id })

        return created?.id ? Number(created.id) : null
    }

    server.get("/banking/iban/:iban", async (req, reply) => {
        try {
            const { iban } = req.params as { iban: string }
            const normalized = normalizeIban(iban)
            if (!normalized) {
                return reply.code(400).send({ error: "IBAN missing" })
            }

            const valid = isValidIbanLocal(normalized)
            const bankData = resolveGermanBankDataFromIbanLocal(normalized)

            return reply.send({
                iban: normalized,
                valid,
                bic: bankData?.bic || null,
                bankName: bankData?.bankName || null,
                bankCode: bankData?.bankCode || null,
            })
        } catch (err) {
            server.log.error(err)
            return reply.code(500).send({ error: "Failed to resolve IBAN data" })
        }
    })

    server.get("/banking/statements/:id/suggestions", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const { id } = req.params as { id: string }
            const statementId = Number(id)
            if (!statementId) return reply.code(400).send({ error: "Invalid statement id" })

            const [statement] = await server.db
                .select()
                .from(bankstatements)
                .where(and(eq(bankstatements.id, statementId), eq(bankstatements.tenant, req.user.tenant_id)))
                .limit(1)

            if (!statement) return reply.code(404).send({ error: "Statement not found" })

            const partnerType: "customer" | "vendor" = Number(statement.amount) >= 0 ? "customer" : "vendor"
            const partnerRef = pickPartnerReference(statement, partnerType)

            const suggestions: Array<Record<string, any>> = []
            let matchedBankAccountId: number | null = null

            if (partnerRef?.iban) {
                const allAccounts = await server.db
                    .select({
                        id: entitybankaccounts.id,
                        ibanEncrypted: entitybankaccounts.ibanEncrypted,
                    })
                    .from(entitybankaccounts)
                    .where(eq(entitybankaccounts.tenant, req.user.tenant_id))

                const matchingAccount = allAccounts.find((row) => {
                    if (!row.ibanEncrypted) return false
                    try {
                        return normalizeIban(decrypt(row.ibanEncrypted as any)) === partnerRef.iban
                    } catch {
                        return false
                    }
                })

                matchedBankAccountId = matchingAccount?.id ? Number(matchingAccount.id) : null
            }

            if (partnerType === "customer") {
                const customerRows = await server.db
                    .select({
                        id: customers.id,
                        name: customers.name,
                        customerNumber: customers.customerNumber,
                        infoData: customers.infoData,
                    })
                    .from(customers)
                    .where(and(eq(customers.tenant, req.user.tenant_id), eq(customers.archived, false)))

                for (const row of customerRows) {
                    const infoData = row.infoData && typeof row.infoData === "object" ? row.infoData as Record<string, any> : {}
                    const bankAccountIds = Array.isArray(infoData.bankAccountIds) ? infoData.bankAccountIds.map(Number) : []
                    const bankingIbans = Array.isArray(infoData.bankingIbans) ? infoData.bankingIbans.map((iban) => normalizeIban(String(iban))) : []
                    const normalizedEntityName = normalizeName(row.name)
                    const normalizedStatementName = normalizeName(partnerRef?.name)

                    const matchesBankAccountId = matchedBankAccountId ? bankAccountIds.includes(matchedBankAccountId) : false
                    const matchesIban = partnerRef?.iban ? bankingIbans.includes(partnerRef.iban) : false
                    const exactNameMatch = normalizedEntityName && normalizedStatementName && normalizedEntityName === normalizedStatementName
                    const partialNameMatch = normalizedEntityName && normalizedStatementName
                        ? normalizedEntityName.includes(normalizedStatementName) || normalizedStatementName.includes(normalizedEntityName)
                        : false

                    let score = 0
                    let reason = ""

                    if (matchesBankAccountId && matchesIban) {
                        score = 100
                        reason = "IBAN und hinterlegte Bankverbindung stimmen überein"
                    } else if (matchesBankAccountId) {
                        score = 95
                        reason = "Hinterlegte Bankverbindung passt zur IBAN"
                    } else if (matchesIban) {
                        score = 90
                        reason = "IBAN wurde bereits bei diesem Kunden verwendet"
                    } else if (exactNameMatch) {
                        score = 60
                        reason = "Name passt exakt zur Buchung"
                    } else if (partialNameMatch) {
                        score = 45
                        reason = "Name ähnelt der Buchung"
                    }

                    if (!score) continue

                    suggestions.push({
                        type: "customer",
                        id: row.id,
                        name: row.name,
                        number: row.customerNumber,
                        score,
                        reason,
                    })
                }
            } else {
                const vendorRows = await server.db
                    .select({
                        id: vendors.id,
                        name: vendors.name,
                        vendorNumber: vendors.vendorNumber,
                        infoData: vendors.infoData,
                    })
                    .from(vendors)
                    .where(and(eq(vendors.tenant, req.user.tenant_id), eq(vendors.archived, false)))

                for (const row of vendorRows) {
                    const infoData = row.infoData && typeof row.infoData === "object" ? row.infoData as Record<string, any> : {}
                    const bankAccountIds = Array.isArray(infoData.bankAccountIds) ? infoData.bankAccountIds.map(Number) : []
                    const bankingIbans = Array.isArray(infoData.bankingIbans) ? infoData.bankingIbans.map((iban) => normalizeIban(String(iban))) : []
                    const normalizedEntityName = normalizeName(row.name)
                    const normalizedStatementName = normalizeName(partnerRef?.name)

                    const matchesBankAccountId = matchedBankAccountId ? bankAccountIds.includes(matchedBankAccountId) : false
                    const matchesIban = partnerRef?.iban ? bankingIbans.includes(partnerRef.iban) : false
                    const exactNameMatch = normalizedEntityName && normalizedStatementName && normalizedEntityName === normalizedStatementName
                    const partialNameMatch = normalizedEntityName && normalizedStatementName
                        ? normalizedEntityName.includes(normalizedStatementName) || normalizedStatementName.includes(normalizedEntityName)
                        : false

                    let score = 0
                    let reason = ""

                    if (matchesBankAccountId && matchesIban) {
                        score = 100
                        reason = "IBAN und hinterlegte Bankverbindung stimmen überein"
                    } else if (matchesBankAccountId) {
                        score = 95
                        reason = "Hinterlegte Bankverbindung passt zur IBAN"
                    } else if (matchesIban) {
                        score = 90
                        reason = "IBAN wurde bereits bei diesem Lieferanten verwendet"
                    } else if (exactNameMatch) {
                        score = 60
                        reason = "Name passt exakt zur Buchung"
                    } else if (partialNameMatch) {
                        score = 45
                        reason = "Name ähnelt der Buchung"
                    }

                    if (!score) continue

                    suggestions.push({
                        type: "vendor",
                        id: row.id,
                        name: row.name,
                        number: row.vendorNumber,
                        score,
                        reason,
                    })
                }
            }

            suggestions.sort((a, b) => b.score - a.score || String(a.name).localeCompare(String(b.name), "de"))

            return reply.send({
                partnerType,
                partnerName: partnerRef?.name || null,
                partnerIban: partnerRef?.iban || null,
                suggestions: suggestions.slice(0, 5),
            })
        } catch (err) {
            server.log.error(err)
            return reply.code(500).send({ error: "Failed to load statement suggestions" })
        }
    })

    const assignIbanFromStatementToCustomer = async (tenantId: number, userId: string, statementId: number, createdDocumentId?: number) => {
        if (!createdDocumentId) return

        const [statement] = await server.db
            .select()
            .from(bankstatements)
            .where(and(eq(bankstatements.id, statementId), eq(bankstatements.tenant, tenantId)))
            .limit(1)

        if (!statement) return

        const [doc] = await server.db
            .select({ customer: createddocuments.customer })
            .from(createddocuments)
            .where(and(eq(createddocuments.id, createdDocumentId), eq(createddocuments.tenant, tenantId)))
            .limit(1)

        const customerId = doc?.customer
        if (!customerId) return

        const partnerBank = pickPartnerBankData(statement, "customer")
        if (!partnerBank?.iban) return

        const [customer] = await server.db
            .select({ id: customers.id, infoData: customers.infoData })
            .from(customers)
            .where(and(eq(customers.id, customerId), eq(customers.tenant, tenantId)))
            .limit(1)

        if (!customer) return

        const bankAccountId = await resolveEntityBankAccountId(
            tenantId,
            userId,
            partnerBank.iban
        )

        const newInfoData = mergePartnerIban(
            (customer.infoData || {}) as Record<string, any>,
            partnerBank.iban,
            bankAccountId
        )
        await server.db
            .update(customers)
            .set({
                infoData: newInfoData,
                updatedAt: new Date(),
                updatedBy: userId,
            })
            .where(and(eq(customers.id, customerId), eq(customers.tenant, tenantId)))
    }

    const assignIbanFromStatementToVendor = async (tenantId: number, userId: string, statementId: number, incomingInvoiceId?: number) => {
        if (!incomingInvoiceId) return

        const [statement] = await server.db
            .select()
            .from(bankstatements)
            .where(and(eq(bankstatements.id, statementId), eq(bankstatements.tenant, tenantId)))
            .limit(1)

        if (!statement) return

        const [invoice] = await server.db
            .select({ vendor: incominginvoices.vendor })
            .from(incominginvoices)
            .where(and(eq(incominginvoices.id, incomingInvoiceId), eq(incominginvoices.tenant, tenantId)))
            .limit(1)

        const vendorId = invoice?.vendor
        if (!vendorId) return

        const partnerBank = pickPartnerBankData(statement, "vendor")
        if (!partnerBank?.iban) return

        const [vendor] = await server.db
            .select({ id: vendors.id, infoData: vendors.infoData })
            .from(vendors)
            .where(and(eq(vendors.id, vendorId), eq(vendors.tenant, tenantId)))
            .limit(1)

        if (!vendor) return

        const bankAccountId = await resolveEntityBankAccountId(
            tenantId,
            userId,
            partnerBank.iban
        )

        const newInfoData = mergePartnerIban(
            (vendor.infoData || {}) as Record<string, any>,
            partnerBank.iban,
            bankAccountId
        )
        await server.db
            .update(vendors)
            .set({
                infoData: newInfoData,
                updatedAt: new Date(),
                updatedBy: userId,
            })
            .where(and(eq(vendors.id, vendorId), eq(vendors.tenant, tenantId)))
    }

    // ------------------------------------------------------------------
    // 🔐 GoCardLess Token Handling
    // ------------------------------------------------------------------

    const goCardLessBaseUrl = secrets.GOCARDLESS_BASE_URL
    const goCardLessSecretId = secrets.GOCARDLESS_SECRET_ID
    const goCardLessSecretKey = secrets.GOCARDLESS_SECRET_KEY

    let tokenData: any = null

    const getToken = async () => {
        const res = await axios.post(`${goCardLessBaseUrl}/token/new/`, {
            secret_id: goCardLessSecretId,
            secret_key: goCardLessSecretKey,
        })

        tokenData = res.data
        tokenData.created_at = new Date().toISOString()

        server.log.info("GoCardless token refreshed.")
    }

    const checkToken = async () => {
        if (!tokenData) return await getToken()

        const expired = dayjs(tokenData.created_at)
            .add(tokenData.access_expires, "seconds")
            .isBefore(dayjs())

        if (expired) {
            server.log.info("Refreshing expired GoCardless token …")
            await getToken()
        }
    }

    // ------------------------------------------------------------------
    // 🔗 Create GoCardless Banking Link
    // ------------------------------------------------------------------
    server.get("/banking/link/:institutionid", async (req, reply) => {
        try {
            await checkToken()

            const { institutionid } = req.params as { institutionid: string }
            const tenantId = req.user?.tenant_id

            if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })

            const { data } = await axios.post(
                `${goCardLessBaseUrl}/requisitions/`,
                {
                    redirect: "https://app.fedeo.de/settings/banking",
                    institution_id: institutionid,
                    user_language: "de",
                },
                {
                    headers: { Authorization: `Bearer ${tokenData.access}` },
                }
            )

            // DB: Requisition speichern
            await server.db.insert(bankrequisitions).values({
                id: data.id,
                tenant: tenantId,
                institutionId: institutionid,
                status: data.status,
            })

            return reply.send({ link: data.link })
        } catch (err: any) {
            server.log.error(err?.response?.data || err)
            return reply.code(500).send({ error: "Failed to generate link" })
        }
    })

    // ------------------------------------------------------------------
    // 🏦 Check Bank Institutions
    // ------------------------------------------------------------------
    server.get("/banking/institutions/:bic", async (req, reply) => {
        try {
            const { bic } = req.params as { bic: string }
            if (!bic) return reply.code(400).send("BIC missing")

            await checkToken()

            const { data } = await axios.get(
                `${goCardLessBaseUrl}/institutions/?country=de`,
                { headers: { Authorization: `Bearer ${tokenData.access}` } }
            )

            const bank = data.find((i: any) => i.bic.toLowerCase() === bic.toLowerCase())

            if (!bank) return reply.code(404).send("Bank not found")

            return reply.send(bank)
        } catch (err: any) {
            server.log.error(err?.response?.data || err)
            return reply.code(500).send("Failed to fetch institutions")
        }
    })


    // ------------------------------------------------------------------
    // 📄 Get Requisition Details
    // ------------------------------------------------------------------
    server.get("/banking/requisitions/:reqId", async (req, reply) => {
        try {
            const { reqId } = req.params as { reqId: string }
            if (!reqId) return reply.code(400).send("Requisition ID missing")

            await checkToken()

            const { data } = await axios.get(
                `${goCardLessBaseUrl}/requisitions/${reqId}`,
                { headers: { Authorization: `Bearer ${tokenData.access}` } }
            )

            // Load account details
            if (data.accounts) {
                data.accounts = await Promise.all(
                    data.accounts.map(async (accId: string) => {
                        const { data: acc } = await axios.get(
                            `${goCardLessBaseUrl}/accounts/${accId}`,
                            { headers: { Authorization: `Bearer ${tokenData.access}` } }
                        )
                        return acc
                    })
                )
            }

            return reply.send(data)
        } catch (err: any) {
            server.log.error(err?.response?.data || err)
            return reply.code(500).send("Failed to fetch requisition details")
        }
    })

    // ------------------------------------------------------------------
    // 📒 List Manual Statement Allocations
    // ------------------------------------------------------------------
    server.get("/banking/manual-bookings", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const rows = await server.db.select({
                allocation: statementallocations,
                account: accounts,
                customer: customers,
                vendor: vendors,
                ownaccount: ownaccounts,
                contraAccount: ContraAccounts,
                contraCustomer: ContraCustomers,
                contraVendor: ContraVendors,
                contraOwnaccount: ContraOwnaccounts,
                incominginvoice: ManualInvoices,
                incominginvoiceVendor: ManualInvoiceVendors,
            })
                .from(statementallocations)
                .leftJoin(accounts, eq(statementallocations.account, accounts.id))
                .leftJoin(customers, eq(statementallocations.customer, customers.id))
                .leftJoin(vendors, eq(statementallocations.vendor, vendors.id))
                .leftJoin(ownaccounts, eq(statementallocations.ownaccount, ownaccounts.id))
                .leftJoin(ContraAccounts, eq(statementallocations.contraAccount, ContraAccounts.id))
                .leftJoin(ContraCustomers, eq(statementallocations.contraCustomer, ContraCustomers.id))
                .leftJoin(ContraVendors, eq(statementallocations.contraVendor, ContraVendors.id))
                .leftJoin(ContraOwnaccounts, eq(statementallocations.contraOwnaccount, ContraOwnaccounts.id))
                .leftJoin(ManualInvoices, eq(statementallocations.incominginvoice, ManualInvoices.id))
                .leftJoin(ManualInvoiceVendors, eq(ManualInvoices.vendor, ManualInvoiceVendors.id))
                .where(and(
                    eq(statementallocations.tenant, req.user.tenant_id),
                    eq(statementallocations.archived, false),
                    isNull(statementallocations.bankstatement)
                ))

            return reply.send(rows.map((row) => ({
                ...row.allocation,
                account: row.account,
                customer: row.customer,
                vendor: row.vendor,
                ownaccount: row.ownaccount,
                contraAccount: row.contraAccount,
                contraCustomer: row.contraCustomer,
                contraVendor: row.contraVendor,
                contraOwnaccount: row.contraOwnaccount,
                incominginvoice: row.incominginvoice ? {
                    ...row.incominginvoice,
                    vendor: row.incominginvoiceVendor,
                } : null,
            })))
        } catch (err) {
            console.error(err)
            return reply.code(500).send({ error: "Failed to load manual bookings" })
        }
    })


    // ------------------------------------------------------------------
    // 💰 Create Statement Allocation
    // ------------------------------------------------------------------
    server.post("/banking/statements", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const { data: payload } = req.body as { data: any }
            const prepared = prepareStatementAllocationPayload(payload)
            if (prepared.error) return reply.code(400).send({ error: prepared.error })

            const inserted = await server.db.insert(statementallocations).values({
                ...prepared.data,
                tenant: req.user.tenant_id
            }).returning()

            const createdRecord = inserted[0]

            if (createdRecord?.createddocument) {
                try {
                    await assignIbanFromStatementToCustomer(
                        req.user.tenant_id,
                        req.user.user_id,
                        Number(createdRecord.bankstatement),
                        Number(createdRecord.createddocument)
                    )
                } catch (err) {
                    server.log.warn({ err, allocationId: createdRecord.id }, "Konnte IBAN nicht automatisch beim Kunden hinterlegen")
                }
            }

            if (createdRecord?.incominginvoice) {
                try {
                    await assignIbanFromStatementToVendor(
                        req.user.tenant_id,
                        req.user.user_id,
                        Number(createdRecord.bankstatement),
                        Number(createdRecord.incominginvoice)
                    )
                } catch (err) {
                    server.log.warn({ err, allocationId: createdRecord.id }, "Konnte IBAN nicht automatisch beim Lieferanten hinterlegen")
                }
            }

            if (createdRecord.bankstatement) {
                await insertHistoryItem(server, {
                    entity: "bankstatements",
                    entityId: Number(createdRecord.bankstatement),
                    action: "created",
                    created_by: req.user.user_id,
                    tenant_id: req.user.tenant_id,
                    oldVal: null,
                    newVal: createdRecord,
                    text: "Buchung erstellt",
                })
            }

            return reply.send(createdRecord)

        } catch (err) {
            console.error(err)
            return reply.code(500).send({ error: "Failed to create statement" })
        }
    })


    // ------------------------------------------------------------------
    // 🗑 Delete Statement Allocation
    // ------------------------------------------------------------------
    server.delete("/banking/statements/:id", async (req, reply) => {
        try {
            if (!req.user) return reply.code(401).send({ error: "Unauthorized" })

            const { id } = req.params as { id: string }

            const oldRecord = await server.db
                .select()
                .from(statementallocations)
                .where(eq(statementallocations.id, id))
                .limit(1)

            const old = oldRecord[0]

            if (!old) return reply.code(404).send({ error: "Record not found" })

            await server.db
                .delete(statementallocations)
                .where(eq(statementallocations.id, id))

            if (old.bankstatement) {
                await insertHistoryItem(server, {
                    entity: "bankstatements",
                    entityId: Number(old.bankstatement),
                    action: "deleted",
                    created_by: req.user.user_id,
                    tenant_id: req.user.tenant_id,
                    oldVal: old,
                    newVal: null,
                    text: "Buchung gelöscht",
                })
            }

            return reply.send({ success: true })

        } catch (err) {
            console.error(err)
            return reply.code(500).send({ error: "Failed to delete statement" })
        }
    })

}
