import { FastifyInstance } from "fastify"
import {
    eq,
    ilike,
    asc,
    desc,
    and,
    count,
    inArray,
    or,
    sql,
} from "drizzle-orm"

import { authProfiles, costcentres, customers, entitybankaccounts } from "../../../db/schema";
import { resourceConfig } from "../../utils/resource.config";
import { useNextNumberRangeNumber } from "../../utils/functions";
import { getHistoryEntityLabel, insertHistoryItem } from "../../utils/history";
import { diffObjects } from "../../utils/diff";
import { recalculateServicePricesForTenant } from "../../modules/service-price-recalculation.service";
import { decrypt, encrypt } from "../../utils/crypt";

const PORTAL_ALLOWED_RESOURCES = new Set(["customers", "contracts", "createddocuments", "contracttypes"])
const PORTAL_VISIBLE_DOCUMENT_TYPES = ["invoices", "advanceInvoices", "cancellationInvoices"]
const OUTGOING_SEPA_MANDATE_STATUSES = new Set(["Entwurf", "Aktiv", "Widerrufen", "Abgelaufen"])
const OUTGOING_SEPA_MANDATE_TYPES = new Set(["CORE", "B2B"])
const OUTGOING_SEPA_SEQUENCE_TYPES = new Set(["RCUR", "OOFF", "FRST", "FNAL"])

// -------------------------------------------------------------
// SQL Suche auf mehreren Feldern (Haupttabelle + Relationen)
// -------------------------------------------------------------
function buildSearchCondition(columns: any[], search: string) {
    if (!search || !columns.length) return null

    const normalizeForSearch = (value: string) =>
        value
            .toLowerCase()
            .normalize("NFD")
            .replace(/[\u0300-\u036f]/g, "")
            .replace(/ß/g, "ss")

    const searchTermsRaw = search
        .trim()
        .toLowerCase()
        .split(/\s+/)
        .filter(Boolean)

    const searchTermsNormalized = searchTermsRaw.map(normalizeForSearch)

    const normalizeSqlExpr = (valueExpr: any) => sql`
        lower(
            replace(
                replace(
                    replace(
                        replace(
                            replace(
                                replace(
                                    replace(cast(${valueExpr} as text), 'Ä', 'A'),
                                    'Ö', 'O'
                                ),
                                'Ü', 'U'
                            ),
                            'ä', 'a'
                        ),
                        'ö', 'o'
                    ),
                    'ü', 'u'
                ),
                'ß', 'ss'
            )
        )
    `

    const validColumns = columns.filter(Boolean)
    if (validColumns.length === 0) return null

    // Alle Suchspalten zu einem String zusammenführen, damit Vor-/Nachname zuverlässig
    // gemeinsam durchsuchbar sind (auch wenn in getrennten Feldern gespeichert).
    const combinedRawExpr = sql`concat_ws(' ', ${sql.join(validColumns.map((col) => sql`coalesce(cast(${col} as text), '')`), sql`, `)})`
    const combinedNormalizedExpr = normalizeSqlExpr(combinedRawExpr)

    const perTermConditions = searchTermsRaw.map((rawTerm, idx) => {
        const normalizedTerm = searchTermsNormalized[idx]
        const rawLike = `%${rawTerm}%`
        const normalizedLike = `%${normalizedTerm}%`

        const rawCondition = ilike(combinedRawExpr, rawLike)
        const normalizedCondition = sql`${combinedNormalizedExpr} like ${normalizedLike}`

        return or(rawCondition, normalizedCondition)
    })

    if (perTermConditions.length === 0) return null
    return and(...perTermConditions)
}

function formatDiffValue(value: any): string {
    if (value === null || value === undefined) return "-"
    if (typeof value === "boolean") return value ? "Ja" : "Nein"
    if (typeof value === "object") {
        try {
            return JSON.stringify(value)
        } catch {
            return "[Objekt]"
        }
    }
    return String(value)
}

const TECHNICAL_HISTORY_KEYS = new Set([
    "id",
    "tenant",
    "tenant_id",
    "createdAt",
    "created_at",
    "createdBy",
    "created_by",
    "updatedAt",
    "updated_at",
    "updatedBy",
    "updated_by",
    "archived",
])

function getUserVisibleChanges(oldRecord: Record<string, any>, updated: Record<string, any>) {
    return diffObjects(oldRecord, updated).filter((c) => !TECHNICAL_HISTORY_KEYS.has(c.key))
}

function buildFieldUpdateHistoryText(resource: string, label: string, oldValue: any, newValue: any) {
    const resourceLabel = getHistoryEntityLabel(resource)
    return `${resourceLabel}: ${label} geändert von "${formatDiffValue(oldValue)}" zu "${formatDiffValue(newValue)}"`
}

function applyResourceWhereFilters(resource: string, table: any, whereCond: any) {
    if (resource === "members") {
        return and(whereCond, eq(table.type, "Mitglied"))
    }
    return whereCond
}

async function getPortalCustomerId(server: FastifyInstance, req: any) {
    const tenantId = req.user?.tenant_id
    const userId = req.user?.user_id

    if (!tenantId || !userId) return null

    const [profile] = await server.db
        .select({ customer_for_portal: authProfiles.customer_for_portal })
        .from(authProfiles)
        .where(and(
            eq(authProfiles.tenant_id, tenantId),
            eq(authProfiles.user_id, userId)
        ))
        .limit(1)

    return profile?.customer_for_portal || null
}

function applyPortalScope(resource: string, table: any, whereCond: any, portalCustomerId: number | null) {
    if (!portalCustomerId) return whereCond

    if (!PORTAL_ALLOWED_RESOURCES.has(resource)) {
        return null
    }

    if (resource === "customers") {
        return and(whereCond, eq(table.id, portalCustomerId))
    }

    if (resource === "contracts") {
        return and(whereCond, eq(table.customer, portalCustomerId))
    }

    if (resource === "createddocuments") {
        return and(
            whereCond,
            eq(table.customer, portalCustomerId),
            eq(table.availableInPortal, true),
            inArray(table.type, PORTAL_VISIBLE_DOCUMENT_TYPES)
        )
    }

    return whereCond
}

function sanitizePortalCustomerUpdate(payload: Record<string, any>) {
    const nextInfoData = payload.infoData && typeof payload.infoData === "object" ? payload.infoData : {}

    return {
        name: payload.name,
        firstname: payload.firstname,
        lastname: payload.lastname,
        salutation: payload.salutation,
        title: payload.title,
        nameAddition: payload.nameAddition,
        infoData: nextInfoData,
    }
}

function getTenantColumn(resource: string, table: any) {
    const config = resourceConfig[resource]
    const tenantKey = config?.tenantKey || "tenant"
    return table[tenantKey]
}

function getRelationConfig(relation: string) {
    const candidateKeys = [
        relation,
        `${relation}s`,
    ]

    if (relation.endsWith("y")) {
        candidateKeys.push(`${relation.slice(0, -1)}ies`)
    }

    if (/(s|x|z|ch|sh)$/.test(relation)) {
        candidateKeys.push(`${relation}es`)
    }

    for (const key of candidateKeys) {
        if (resourceConfig[key]) return resourceConfig[key]
    }

    return null
}

function isDateLikeField(key: string) {
    if (key === "deliveryDateType") return false
    if (key.includes("_at") || key.endsWith("At")) return true
    if (/Date$/.test(key)) return true
    return /(^|_|-)date($|_|-)/i.test(key)
}

function isDateValue(value: any) {
    if (value instanceof Date) return !Number.isNaN(value.getTime())
    if (typeof value !== "string") return false
    const normalized = value.trim()
    if (/^\d+$/.test(normalized)) return false
    return !Number.isNaN(new Date(normalized).getTime())
}

function normalizeCreatedDocumentPayload(payload: Record<string, any>) {
    if (Object.prototype.hasOwnProperty.call(payload, "customSurchargePercentage")) {
        const customSurchargePercentage = Number(payload.customSurchargePercentage)
        payload.customSurchargePercentage = Number.isFinite(customSurchargePercentage)
            ? customSurchargePercentage
            : 0
    }

    const numberRelationFields = [
        "customer",
        "contact",
        "project",
        "createddocument",
        "letterhead",
        "plant",
        "contract",
        "outgoingsepamandate",
    ]

    for (const field of numberRelationFields) {
        const value = payload[field]
        if (value instanceof Date || (typeof value === "string" && isDateValue(value))) {
            payload[field] = null
        }
    }

    const serialexecution = payload.serialexecution
    if (serialexecution === undefined || serialexecution === null || serialexecution === "") return payload

    if (isDateValue(serialexecution)) {
        payload.serialexecution = null
        return payload
    }

    if (typeof serialexecution === "string") {
        const normalized = serialexecution.trim()
        const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(normalized)
        if (!isUuid && isDateValue(normalized)) {
            payload.serialexecution = null
            return payload
        }
    }

    return payload
}

function normalizeMemberPayload(payload: Record<string, any>) {
    const infoData = payload.infoData && typeof payload.infoData === "object" ? payload.infoData : {}
    const normalized = {
        ...payload,
        type: "Mitglied",
        isCompany: false,
        infoData,
    }

    return normalized
}

function validateMemberPayload(payload: Record<string, any>) {
    const infoData = payload.infoData && typeof payload.infoData === "object" ? payload.infoData : {}
    const bankAccountIds = Array.isArray(infoData.bankAccountIds) ? infoData.bankAccountIds.filter(Boolean) : []
    const firstname = typeof payload.firstname === "string" ? payload.firstname.trim() : ""
    const lastname = typeof payload.lastname === "string" ? payload.lastname.trim() : ""

    if (!firstname || !lastname) {
        return "Für Mitglieder sind Vorname und Nachname erforderlich."
    }

    if (!bankAccountIds.length) {
        return "Für Mitglieder muss mindestens ein Bankkonto hinterlegt werden."
    }

    if (infoData.hasSEPA && !infoData.sepaSignedAt) {
        return "Wenn ein SEPA-Mandat hinterlegt ist, muss ein Unterschriftsdatum gesetzt werden."
    }

    return null
}

async function validateCostCentreParent(
    server: FastifyInstance,
    tenantId: number,
    costCentreId: string | null,
    parentCostcentreId: string | null
) {
    if (!parentCostcentreId) {
        return null
    }

    const hierarchyRows = await server.db
        .select({
            id: costcentres.id,
            parentCostcentre: costcentres.parentCostcentre,
        })
        .from(costcentres)
        .where(eq(costcentres.tenant, tenantId))

    const hierarchyMap = new Map(
        hierarchyRows.map((row) => [row.id, row.parentCostcentre || null])
    )

    if (!hierarchyMap.has(parentCostcentreId)) {
        return "Die übergeordnete Kostenstelle wurde nicht gefunden."
    }

    if (costCentreId && parentCostcentreId === costCentreId) {
        return "Eine Kostenstelle kann nicht sich selbst als übergeordnete Kostenstelle haben."
    }

    if (!costCentreId) {
        return null
    }

    let currentParentId: string | null = parentCostcentreId
    const visited = new Set<string>()

    while (currentParentId) {
        if (currentParentId === costCentreId) {
            return "Die ausgewählte übergeordnete Kostenstelle würde einen Zyklus erzeugen."
        }

        if (visited.has(currentParentId)) {
            break
        }

        visited.add(currentParentId)
        currentParentId = hierarchyMap.get(currentParentId) || null
    }

    return null
}

function maskIban(iban: string) {
    if (!iban) return ""
    const cleaned = iban.replace(/\s+/g, "")
    if (cleaned.length <= 8) return cleaned
    return `${cleaned.slice(0, 4)} **** **** ${cleaned.slice(-4)}`
}

function decryptEntityBankAccount(row: Record<string, any>) {
    let iban = null
    let bic = null
    let bankName = null
    let decryptError = null

    try {
        iban = row.ibanEncrypted ? decrypt(row.ibanEncrypted as any) : null
        bic = row.bicEncrypted ? decrypt(row.bicEncrypted as any) : null
        bankName = row.bankNameEncrypted ? decrypt(row.bankNameEncrypted as any) : null
    } catch (err: any) {
        decryptError = err?.message || "Bankverbindung konnte nicht entschlüsselt werden."
    }

    return {
        ...row,
        iban,
        bic,
        bankName,
        decryptError,
        displayLabel: decryptError
            ? `Bankverbindung nicht lesbar${row.description ? ` (${row.description})` : ""}`
            : `${maskIban(iban || "")}${bankName ? ` | ${bankName}` : ""}${row.description ? ` (${row.description})` : ""}`.trim(),
    }
}

function prepareEntityBankAccountPayload(payload: Record<string, any>, requireAll: boolean) {
    const iban = typeof payload.iban === "string" ? payload.iban.trim() : ""
    const bic = typeof payload.bic === "string" ? payload.bic.trim() : ""
    const bankName = typeof payload.bankName === "string" ? payload.bankName.trim() : ""

    const hasAnyPlainField = Object.prototype.hasOwnProperty.call(payload, "iban")
        || Object.prototype.hasOwnProperty.call(payload, "bic")
        || Object.prototype.hasOwnProperty.call(payload, "bankName")

    if (!hasAnyPlainField && !requireAll) {
        return { data: payload }
    }

    if (!iban || !bic || !bankName) {
        return { error: "IBAN, BIC und Bankinstitut sind Pflichtfelder." }
    }

    const result: Record<string, any> = {
        ...payload,
        ibanEncrypted: encrypt(iban),
        bicEncrypted: encrypt(bic),
        bankNameEncrypted: encrypt(bankName),
    }

    delete result.iban
    delete result.bic
    delete result.bankName

    return { data: result }
}

async function validateOutgoingSepaMandatePayload(
    server: FastifyInstance,
    tenantId: number,
    payload: Record<string, any>,
    existing: Record<string, any> | null = null
) {
    const customerId = Number(payload.customer ?? existing?.customer)
    const bankaccountId = Number(payload.bankaccount ?? existing?.bankaccount)

    if (!customerId || !bankaccountId) {
        return "Kunde und Bankverbindung sind Pflichtfelder."
    }

    const status = payload.status ?? existing?.status ?? "Entwurf"
    if (!OUTGOING_SEPA_MANDATE_STATUSES.has(status)) {
        return "Ungültiger Mandatsstatus."
    }

    const mandateType = payload.mandateType ?? existing?.mandateType ?? "CORE"
    if (!OUTGOING_SEPA_MANDATE_TYPES.has(mandateType)) {
        return "Ungültiger Mandatstyp."
    }

    const sequenceType = payload.sequenceType ?? existing?.sequenceType ?? "RCUR"
    if (!OUTGOING_SEPA_SEQUENCE_TYPES.has(sequenceType)) {
        return "Ungültige Mandatssequenz."
    }

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

    if (!customer) {
        return "Kunde nicht gefunden."
    }

    const [bankaccount] = await server.db
        .select()
        .from(entitybankaccounts)
        .where(and(eq(entitybankaccounts.id, bankaccountId), eq(entitybankaccounts.tenant, tenantId)))
        .limit(1)

    if (!bankaccount) {
        return "Bankverbindung nicht gefunden."
    }

    const assignedBankAccountIds = Array.isArray((customer.infoData as any)?.bankAccountIds)
        ? (customer.infoData as any).bankAccountIds.map((id: any) => Number(id))
        : []

    if (!assignedBankAccountIds.includes(bankaccountId)) {
        return "Die Bankverbindung ist dem ausgewählten Kunden nicht zugeordnet."
    }

    return null
}

export default async function resourceRoutes(server: FastifyInstance) {

    // -------------------------------------------------------------
    // LIST
    // -------------------------------------------------------------
    server.get("/resource/:resource", async (req, reply) => {
        try {
            const tenantId = req.user?.tenant_id
            if (!tenantId)
                return reply.code(400).send({ error: "No tenant selected" })

            const { search, sort, asc: ascQuery } = req.query as {
                search?: string
                sort?: string
                asc?: string
            }

            const { resource } = req.params as { resource: string }
            const config = resourceConfig[resource]
            if (!config) {
                return reply.code(404).send({ error: "Unknown resource" })
            }
            const portalCustomerId = await getPortalCustomerId(server, req)
            if (portalCustomerId && !PORTAL_ALLOWED_RESOURCES.has(resource)) {
                return reply.code(403).send({ error: "Forbidden" })
            }
            const table = config.table

            const tenantColumn = getTenantColumn(resource, table)
            let whereCond: any = tenantColumn ? eq(tenantColumn, tenantId) : undefined
            whereCond = applyResourceWhereFilters(resource, table, whereCond)
            whereCond = applyPortalScope(resource, table, whereCond, portalCustomerId)
            let q = server.db.select().from(table).$dynamic()

            const searchCols: any[] = (config.searchColumns || []).map(c => table[c])

            if (config.mtoLoad) {
                config.mtoLoad.forEach(rel => {
                    const relConfig = getRelationConfig(rel)
                    if (relConfig) {
                        const relTable = relConfig.table

                        // FIX: Nur joinen, wenn es keine Self-Reference ist (verhindert ERROR 42712)
                        if (relTable !== table) {
                            // @ts-ignore
                            q = q.leftJoin(relTable, eq(table[rel], relTable.id))
                            if (relConfig.searchColumns) {
                                relConfig.searchColumns.forEach(c => {
                                    if (relTable[c]) searchCols.push(relTable[c])
                                })
                            }
                        }
                    }
                })
            }

            if (search) {
                const searchCond = buildSearchCondition(searchCols, search.trim())
                if (searchCond) whereCond = and(whereCond, searchCond)
            }

            q = q.where(whereCond)

            if (sort) {
                const col = (table as any)[sort]
                if (col) {
                    q = ascQuery === "true" ? q.orderBy(asc(col)) : q.orderBy(desc(col))
                }
            }

            const queryData = await q
            // Transformation: Falls Joins genutzt wurden, das Hauptobjekt extrahieren
            const rows = queryData.map(r => r[resource] || r.table || r);

            // RELATION LOADING
            let data = [...rows]
            if(config.mtoLoad) {
                let ids: any = {}
                let lists: any = {}
                let maps: any = {}
                config.mtoLoad.forEach(rel => {
                    ids[rel] = [...new Set(rows.map(r => r[rel]).filter(Boolean))];
                })
                for await (const rel of config.mtoLoad) {
                    const relConf = getRelationConfig(rel)
                    if (!relConf) continue
                    const relTab = relConf.table
                    lists[rel] = ids[rel].length ? await server.db.select().from(relTab).where(inArray(relTab.id, ids[rel])) : []
                    maps[rel] = Object.fromEntries(lists[rel].map((i: any) => [i.id, i]));
                }
                data = rows.map(row => {
                    let toReturn = { ...row }
                    config.mtoLoad.forEach(rel => {
                        toReturn[rel] = row[rel] ? maps[rel][row[rel]] : null
                    })
                    return toReturn
                });
            }

            if(config.mtmListLoad) {
                for await (const relation of config.mtmListLoad) {
                    const relTable = resourceConfig[relation].table
                    const parentKey = config.relationKey || resource.substring(0, resource.length - 1)
                    const relationRows = await server.db.select().from(relTable).where(inArray(relTable[parentKey], data.map(i => i.id)))
                    data = data.map(row => ({
                        ...row,
                        [relation]: relationRows.filter(i => i[parentKey] === row.id)
                    }))
                }
            }

            if (resource === "entitybankaccounts") {
                return data.map((row) => decryptEntityBankAccount(row))
            }

            return data

        } catch (err) {
            console.error("ERROR /resource/:resource", err)
            return reply.code(500).send({ error: "Internal Server Error" })
        }
    })

    // -------------------------------------------------------------
    // PAGINATED LIST
    // -------------------------------------------------------------
    server.get("/resource/:resource/paginated", async (req, reply) => {
        try {
            const tenantId = req.user?.tenant_id;
            if (!tenantId) return reply.code(400).send({ error: "No tenant selected" });

            const { resource } = req.params as { resource: string };
            const config = resourceConfig[resource];
            if (!config) {
                return reply.code(404).send({ error: "Unknown resource" });
            }
            const portalCustomerId = await getPortalCustomerId(server, req)
            if (portalCustomerId && !PORTAL_ALLOWED_RESOURCES.has(resource)) {
                return reply.code(403).send({ error: "Forbidden" })
            }
            const table = config.table;

            const { queryConfig } = req;
            const { pagination, sort, filters } = queryConfig;
            const { search, distinctColumns } = req.query as { search?: string; distinctColumns?: string; };

            const tenantColumn = getTenantColumn(resource, table);
            let whereCond: any = tenantColumn ? eq(tenantColumn, tenantId) : undefined;
            whereCond = applyResourceWhereFilters(resource, table, whereCond)
            whereCond = applyPortalScope(resource, table, whereCond, portalCustomerId)
            const searchCols: any[] = (config.searchColumns || []).map(c => table[c]);
            const debugSearchColumnNames: string[] = [...(config.searchColumns || [])];
            const parsedFilters: Array<{ key: string; value: any }> = []

            let countQuery = server.db.select({ value: count(table.id) }).from(table).$dynamic();
            let mainQuery = server.db.select().from(table).$dynamic();

            if (config.mtoLoad) {
                config.mtoLoad.forEach(rel => {
                    const relConfig = getRelationConfig(rel)
                    if (relConfig) {
                        const relTable = relConfig.table;

                        // FIX: Self-Reference Check
                        if (relTable !== table) {
                            countQuery = countQuery.leftJoin(relTable, eq(table[rel], relTable.id));
                            // @ts-ignore
                            mainQuery = mainQuery.leftJoin(relTable, eq(table[rel], relTable.id));
                            if (relConfig.searchColumns) {
                                relConfig.searchColumns.forEach(c => {
                                    if (relTable[c]) {
                                        searchCols.push(relTable[c]);
                                        debugSearchColumnNames.push(`${rel}.${c}`);
                                    }
                                });
                            }
                        }
                    }
                });
            }

            if (search) {
                if (resource === "customers") {
                    const rawSearch = search.trim()
                    const terms = rawSearch.toLowerCase().split(/\s+/).filter(Boolean)
                    const normalizedTerms = terms
                        .map((t) => t.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/ß/g, "ss"))

                    server.log.info({
                        tag: "customer-search-debug",
                        search: rawSearch,
                        terms,
                        normalizedTerms,
                        searchColumns: debugSearchColumnNames,
                        page: pagination?.page ?? 1,
                        limit: pagination?.limit ?? 100,
                    }, "Paginated customer search request")
                }

                const searchCond = buildSearchCondition(searchCols, search.trim());
                if (searchCond) whereCond = and(whereCond, searchCond);
            }

            if (filters) {
                for (const [key, val] of Object.entries(filters)) {
                    const col = (table as any)[key];
                    if (!col) continue;
                    parsedFilters.push({ key, value: val })
                    whereCond = Array.isArray(val) ? and(whereCond, inArray(col, val)) : and(whereCond, eq(col, val as any));
                }
            }

            const totalRes = await countQuery.where(whereCond);
            const total = Number(totalRes[0]?.value ?? 0);

            const offset = pagination?.offset ?? 0;
            const limit = pagination?.limit ?? 100;

            mainQuery = mainQuery.where(whereCond).offset(offset).limit(limit);

            if (sort?.length > 0) {
                const s = sort[0];
                const col = (table as any)[s.field];
                if (col) {
                    mainQuery = s.direction === "asc" ? mainQuery.orderBy(asc(col)) : mainQuery.orderBy(desc(col));
                }
            }

            const rawRows = await mainQuery;
            // Transformation für Drizzle Joins
            let rows = rawRows.map(r => r[resource] || r.table || r);

            const distinctValues: Record<string, any[]> = {};
            if (distinctColumns) {
                for (const colName of distinctColumns.split(",").map(c => c.trim())) {
                    const col = (table as any)[colName];
                    if (!col) continue;
                    let distinctQuery = server.db.select({ v: col }).from(table).$dynamic();
                    if (config.mtoLoad) {
                        config.mtoLoad.forEach(rel => {
                            const relConfig = getRelationConfig(rel)
                            if (!relConfig) return;
                            const relTable = relConfig.table;
                            if (relTable !== table) {
                                distinctQuery = distinctQuery.leftJoin(relTable, eq(table[rel], relTable.id));
                            }
                        });
                    }
                    let distinctWhereCond: any = tenantColumn ? eq(tenantColumn, tenantId) : undefined
                    distinctWhereCond = applyResourceWhereFilters(resource, table, distinctWhereCond)
                    distinctWhereCond = applyPortalScope(resource, table, distinctWhereCond, portalCustomerId)

                    if (search) {
                        const searchCond = buildSearchCondition(searchCols, search.trim())
                        if (searchCond) distinctWhereCond = and(distinctWhereCond, searchCond)
                    }

                    for (const f of parsedFilters) {
                        if (f.key === colName) continue
                        const filterCol = (table as any)[f.key]
                        if (!filterCol) continue
                        distinctWhereCond = Array.isArray(f.value)
                            ? and(distinctWhereCond, inArray(filterCol, f.value))
                            : and(distinctWhereCond, eq(filterCol, f.value as any))
                    }

                    const dRows = await distinctQuery.where(distinctWhereCond);
                    distinctValues[colName] = [...new Set(dRows.map(r => r.v).filter(v => v != null && v !== ""))].sort();
                }
            }

            let data = [...rows];
            if (config.mtoLoad) {
                let ids: any = {};
                let lists: any = {};
                let maps: any = {};
                config.mtoLoad.forEach(rel => {
                    ids[rel] = [...new Set(rows.map(r => r[rel]).filter(Boolean))];
                });
                for await (const rel of config.mtoLoad) {
                    const relConf = getRelationConfig(rel)
                    if (!relConf) continue
                    const relTab = relConf.table;
                    lists[rel] = ids[rel].length ? await server.db.select().from(relTab).where(inArray(relTab.id, ids[rel])) : [];
                    maps[rel] = Object.fromEntries(lists[rel].map((i: any) => [i.id, i]));
                }
                data = rows.map(row => {
                    let toReturn = { ...row };
                    config.mtoLoad.forEach(rel => {
                        toReturn[rel] = row[rel] ? maps[rel][row[rel]] : null;
                    });
                    return toReturn;
                });
            }

            if (config.mtmListLoad) {
                for await (const relation of config.mtmListLoad) {
                    const relTable = resourceConfig[relation].table;
                    const parentKey = config.relationKey || resource.substring(0, resource.length - 1);
                    const relationRows = await server.db.select().from(relTable).where(inArray(relTable[parentKey], data.map(i => i.id)));
                    data = data.map(row => ({
                        ...row,
                        [relation]: relationRows.filter(i => i[parentKey] === row.id)
                    }));
                }
            }

            if (resource === "entitybankaccounts") {
                data = data.map((row) => decryptEntityBankAccount(row))
            }

            return {
                data,
                queryConfig: { ...queryConfig, total, totalPages: Math.ceil(total / limit), distinctValues }
            };

        } catch (err) {
            console.error(`ERROR /resource/:resource/paginated:`, err);
            return reply.code(500).send({ error: "Internal Server Error" });
        }
    });

    // -------------------------------------------------------------
    // DETAIL
    // -------------------------------------------------------------
    server.get("/resource/:resource/:id/:no_relations?", async (req, reply) => {
        try {
            const { id } = req.params as { id: string }
            const tenantId = req.user?.tenant_id
            if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })

            const { resource, no_relations } = req.params as { resource: string, no_relations?: boolean }
            const portalCustomerId = await getPortalCustomerId(server, req)
            if (portalCustomerId && !PORTAL_ALLOWED_RESOURCES.has(resource)) {
                return reply.code(403).send({ error: "Forbidden" })
            }
            const table = resourceConfig[resource].table

            let whereCond: any = and(eq(table.id, id), eq(table.tenant, tenantId))
            whereCond = applyResourceWhereFilters(resource, table, whereCond)
            whereCond = applyPortalScope(resource, table, whereCond, portalCustomerId)

            const projRows = await server.db
                .select()
                .from(table)
                .where(whereCond)
                .limit(1)

            if (!projRows.length)
                return reply.code(404).send({ error: "Resource not found" })

            let data = { ...projRows[0] }

            if (!no_relations) {
                if (resourceConfig[resource].mtoLoad) {
                    for await (const relation of resourceConfig[resource].mtoLoad) {
                        if (data[relation]) {
                            const relConf = getRelationConfig(relation)
                            if (!relConf) continue
                            const relTable = relConf.table
                            const relData = await server.db.select().from(relTable).where(eq(relTable.id, data[relation]))
                            data[relation] = relData[0] || null
                        }
                    }
                }

                if (resourceConfig[resource].mtmLoad) {
                    for await (const relation of resourceConfig[resource].mtmLoad) {
                        const relTable = resourceConfig[relation].table
                        const parentKey = resourceConfig[resource].relationKey || resource.substring(0, resource.length - 1)
                        data[relation] = await server.db.select().from(relTable).where(eq(relTable[parentKey], id))
                    }
                }
            }

            if (resource === "entitybankaccounts") {
                return decryptEntityBankAccount(data)
            }

            return data
        } catch (err) {
            console.error("ERROR /resource/:resource/:id", err)
            return reply.code(500).send({ error: "Internal Server Error" })
        }
    })

    // Create
    server.post("/resource/:resource", async (req, reply) => {
        try {
            if (!req.user?.tenant_id) return reply.code(400).send({ error: "No tenant selected" });
            const { resource } = req.params as { resource: string };
            const portalCustomerId = await getPortalCustomerId(server, req)
            if (portalCustomerId) {
                return reply.code(403).send({ error: "Forbidden" })
            }
            if (resource === "accounts") {
                return reply.code(403).send({ error: "Accounts are read-only" })
            }
            const body = req.body as Record<string, any>;
            const config = resourceConfig[resource];
            const table = config.table;

            let createData: Record<string, any> = { ...body, tenant: req.user.tenant_id, archived: false };

            if (resource === "members") {
                createData = normalizeMemberPayload(createData)
                const validationError = validateMemberPayload(createData)
                if (validationError) {
                    return reply.code(400).send({ error: validationError })
                }
            }

            if (resource === "entitybankaccounts") {
                const prepared = prepareEntityBankAccountPayload(createData, true)
                if (prepared.error) return reply.code(400).send({ error: prepared.error })
                createData = prepared.data!
            }

            if (resource === "costcentres") {
                const validationError = await validateCostCentreParent(
                    server,
                    req.user.tenant_id,
                    null,
                    createData.parentCostcentre || null
                )

                if (validationError) {
                    return reply.code(400).send({ error: validationError })
                }
            }

            if (resource === "outgoingsepamandates") {
                const validationError = await validateOutgoingSepaMandatePayload(server, req.user.tenant_id, createData)
                if (validationError) {
                    return reply.code(400).send({ error: validationError })
                }
            }

            if (resource === "createddocuments") {
                createData = normalizeCreatedDocumentPayload(createData)
            }

            if (config.numberRangeHolder && !body[config.numberRangeHolder]) {
                const numberRangeResource = resource === "members" ? "customers" : resource
                const result = await useNextNumberRangeNumber(server, req.user.tenant_id, numberRangeResource)
                createData[config.numberRangeHolder] = result.usedNumber
            }

            const normalizeDate = (val: any) => { const d = new Date(val); return isNaN(d.getTime()) ? null : d; }
            Object.keys(createData).forEach((key) => {
                const value = createData[key]
                const shouldNormalize =
                    isDateLikeField(key) &&
                    value !== null &&
                    value !== undefined &&
                    (typeof value === "string" || typeof value === "number" || value instanceof Date)

                if (shouldNormalize) {
                    createData[key] = normalizeDate(value)
                }
            })

            const [created] = await server.db.insert(table).values(createData).returning()

            if (resource === "outgoingsepamandates" && created?.defaultMandate) {
                await server.db
                    .update(table)
                    .set({ defaultMandate: false })
                    .where(and(
                        eq(table.tenant, req.user.tenant_id),
                        eq(table.customer, created.customer),
                        sql`${table.id} <> ${created.id}`
                    ))
            }

            if (["products", "services", "hourrates"].includes(resource)) {
                await recalculateServicePricesForTenant(server, req.user.tenant_id, req.user?.user_id || null);
            }

            if (created) {
                try {
                    const resourceLabel = getHistoryEntityLabel(resource)
                    await insertHistoryItem(server, {
                        tenant_id: req.user.tenant_id,
                        created_by: req.user?.user_id || null,
                        entity: resource,
                        entityId: created.id,
                        action: "created",
                        oldVal: null,
                        newVal: created,
                        text: `Neuer Eintrag in ${resourceLabel} erstellt`,
                    })
                } catch (historyError) {
                    server.log.warn({ err: historyError, resource }, "Failed to write create history entry")
                }
            }

            if (resource === "entitybankaccounts") {
                return decryptEntityBankAccount(created as Record<string, any>)
            }

            return created;
        } catch (error) {
            console.error(error);
            reply.status(500);
        }
    });

    // Update
    server.put("/resource/:resource/:id", async (req, reply) => {
        try {
            const { resource, id } = req.params as { resource: string; id: string }
            if (resource === "accounts") {
                return reply.code(403).send({ error: "Accounts are read-only" })
            }
            const body = req.body as Record<string, any>
            const tenantId = req.user?.tenant_id
            const userId = req.user?.user_id
            const portalCustomerId = await getPortalCustomerId(server, req)

            if (!tenantId || !userId) return reply.code(401).send({ error: "Unauthorized" })
            if (portalCustomerId && resource !== "customers") {
                return reply.code(403).send({ error: "Forbidden" })
            }

            const table = resourceConfig[resource].table
            const normalizeDate = (val: any) => { const d = new Date(val); return isNaN(d.getTime()) ? null : d; }

            const [oldRecord] = await server.db
                .select()
                .from(table)
                .where(applyPortalScope(resource, table, and(eq(table.id, id), eq(table.tenant, tenantId)), portalCustomerId))
                .limit(1)

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

            let data: Record<string, any> = { ...body, updated_at: new Date().toISOString(), updated_by: userId }
            //@ts-ignore
            delete data.updatedBy; delete data.updatedAt;

            if (resource === "filetags") {
                delete data.isSystemUsed

                if (oldRecord.isSystemUsed && data.archived === true) {
                    return reply.code(400).send({ error: "System-Dateitypen können nicht archiviert werden" })
                }
            }

            if (portalCustomerId) {
                data = {
                    ...sanitizePortalCustomerUpdate(data),
                    updated_at: data.updated_at,
                    updated_by: data.updated_by,
                }
            }

            if (resource === "members") {
                data = normalizeMemberPayload(data)
                const validationError = validateMemberPayload(data)
                if (validationError) {
                    return reply.code(400).send({ error: validationError })
                }
            }

            if (resource === "entitybankaccounts") {
                const prepared = prepareEntityBankAccountPayload(data, false)
                if (prepared.error) return reply.code(400).send({ error: prepared.error })
                data = {
                    ...prepared.data,
                    updated_at: data.updated_at,
                    updated_by: data.updated_by,
                }
            }

            if (resource === "costcentres") {
                const validationError = await validateCostCentreParent(
                    server,
                    tenantId,
                    oldRecord.id,
                    Object.prototype.hasOwnProperty.call(data, "parentCostcentre")
                        ? data.parentCostcentre || null
                        : oldRecord.parentCostcentre || null
                )

                if (validationError) {
                    return reply.code(400).send({ error: validationError })
                }
            }

            if (resource === "outgoingsepamandates") {
                const validationError = await validateOutgoingSepaMandatePayload(server, tenantId, data, oldRecord)
                if (validationError) {
                    return reply.code(400).send({ error: validationError })
                }
            }

            if (resource === "createddocuments") {
                data = normalizeCreatedDocumentPayload(data)
            }

            Object.keys(data).forEach((key) => {
                const value = data[key]
                const shouldNormalize =
                    isDateLikeField(key) &&
                    value !== null &&
                    value !== undefined &&
                    (typeof value === "string" || typeof value === "number" || value instanceof Date)

                if (shouldNormalize) {
                    data[key] = normalizeDate(value)
                }
            })

            let updateWhereCond: any = and(eq(table.id, id), eq(table.tenant, tenantId))
            updateWhereCond = applyResourceWhereFilters(resource, table, updateWhereCond)
            const [updated] = await server.db.update(table).set(data).where(updateWhereCond).returning()

            if (resource === "outgoingsepamandates" && updated?.defaultMandate) {
                await server.db
                    .update(table)
                    .set({ defaultMandate: false })
                    .where(and(
                        eq(table.tenant, tenantId),
                        eq(table.customer, updated.customer),
                        sql`${table.id} <> ${updated.id}`
                    ))
            }

            if (["products", "services", "hourrates"].includes(resource)) {
                await recalculateServicePricesForTenant(server, tenantId, userId);
            }

            if (updated) {
                try {
                    const resourceLabel = getHistoryEntityLabel(resource)
                    const changes = oldRecord ? getUserVisibleChanges(oldRecord, updated) : []
                    if (!changes.length) {
                        await insertHistoryItem(server, {
                            tenant_id: tenantId,
                            created_by: userId,
                            entity: resource,
                            entityId: updated.id,
                            action: "updated",
                            oldVal: oldRecord || null,
                            newVal: updated,
                            text: `Eintrag in ${resourceLabel} geändert`,
                        })
                    } else {
                        for (const change of changes) {
                            await insertHistoryItem(server, {
                                tenant_id: tenantId,
                                created_by: userId,
                                entity: resource,
                                entityId: updated.id,
                                action: "updated",
                                oldVal: change.oldValue,
                                newVal: change.newValue,
                                text: buildFieldUpdateHistoryText(resource, change.label, change.oldValue, change.newValue),
                            })
                        }
                    }
                } catch (historyError) {
                    server.log.warn({ err: historyError, resource, id }, "Failed to write update history entry")
                }
            }

            if (resource === "entitybankaccounts") {
                return decryptEntityBankAccount(updated as Record<string, any>)
            }

            return updated
        } catch (err) {
            console.error(err)
        }
    })
}
