import { and, desc, eq, ilike, or } from "drizzle-orm"
import { randomUUID } from "node:crypto"
import {
    accounts,
    bankstatements,
    createddocuments,
    filetags,
    files,
    folders,
    incominginvoices,
    statementallocations,
} from "../../../db/schema"
import { useNextNumberRangeNumber } from "../../utils/functions"
import { saveFile } from "../../utils/files"
import { McpTool } from "../types"

const limitFromArgs = (args: Record<string, unknown>, fallback = 25) => {
    const raw = Number(args.limit ?? fallback)
    if (!Number.isFinite(raw)) return fallback
    return Math.min(Math.max(Math.trunc(raw), 1), 100)
}

const stringArg = (args: Record<string, unknown>, key: string) => {
    const value = args[key]
    return typeof value === "string" && value.trim() ? value.trim() : null
}

const numberArg = (args: Record<string, unknown>, key: string) => {
    const value = Number(args[key])
    return Number.isFinite(value) ? value : null
}

const hasValue = (value: unknown) => value !== null && value !== undefined && value !== ""
const hasValidNumber = (value: unknown) => hasValue(value) && Number.isFinite(Number(value))
const MAX_MCP_UPLOAD_BYTES = 20 * 1024 * 1024

const allowedOutgoingDocumentTypes = new Set([
    "quotes",
    "costEstimates",
    "confirmationOrders",
    "deliveryNotes",
    "packingSlips",
    "invoices",
    "advanceInvoices",
    "cancellationInvoices",
    "serialInvoices",
])

const allowedOutgoingDocumentTaxTypes = new Set([
    "Standard",
    "13b UStG",
    "19 UStG",
    "12.3 UStG",
])

const outgoingDocumentTaxableTypes = new Set([
    "invoices",
    "cancellationInvoices",
    "advanceInvoices",
    "serialInvoices",
    "confirmationOrders",
    "quotes",
    "costEstimates",
])

const documentTypeArg = (args: Record<string, unknown>, key = "type") => {
    const type = stringArg(args, key) || "invoices"
    if (!allowedOutgoingDocumentTypes.has(type)) {
        throw new Error(`Ungültige Belegart: ${type}`)
    }
    return type
}

const normalizeOutgoingDocumentTaxType = (value: unknown) => {
    const taxType = typeof value === "string" && value.trim() ? value.trim() : "Standard"
    if (!allowedOutgoingDocumentTaxTypes.has(taxType)) {
        throw new Error(`Ungültiger Dokument-Steuertyp: ${taxType}`)
    }
    return taxType
}

const applyOutgoingDocumentTaxType = (
    payload: Record<string, unknown>,
    args: Record<string, unknown>,
    documentType: string,
    existingTaxType?: unknown,
    existingRows?: unknown
) => {
    if (!outgoingDocumentTaxableTypes.has(documentType)) {
        payload.taxType = null
        return
    }

    const taxType = args.taxType !== undefined
        ? normalizeOutgoingDocumentTaxType(args.taxType)
        : existingTaxType
            ? normalizeOutgoingDocumentTaxType(existingTaxType)
            : "Standard"

    payload.taxType = taxType

    if (["13b UStG", "19 UStG", "12.3 UStG"].includes(taxType)) {
        const rows = Array.isArray(payload.rows)
            ? payload.rows
            : Array.isArray(existingRows)
                ? normalizeOutgoingDocumentRows(existingRows)
                : null

        if (!rows) return

        payload.rows = rows.map((row: any) => ({
            ...row,
            taxPercent: 0,
        }))
    }
}

const optionalObjectArg = (args: Record<string, unknown>, key: string) => {
    const value = args[key]
    return value && typeof value === "object" && !Array.isArray(value) ? value : null
}

const optionalArrayArg = (args: Record<string, unknown>, key: string) => {
    const value = args[key]
    return Array.isArray(value) ? value : null
}

const normalizeOutgoingDocumentRow = (row: unknown, index: number) => {
    let normalizedRow = row

    if (typeof normalizedRow === "string") {
        try {
            normalizedRow = JSON.parse(normalizedRow)
        } catch {
            throw new Error(`Position ${index + 1} ist kein gültiges JSON-Objekt`)
        }
    }

    if (!normalizedRow || typeof normalizedRow !== "object" || Array.isArray(normalizedRow)) {
        throw new Error(`Position ${index + 1} muss ein Objekt sein`)
    }

    const rowPayload = { ...(normalizedRow as Record<string, any>) }

    rowPayload.id = rowPayload.id || randomUUID()
    rowPayload.pos = rowPayload.pos || String(index + 1)
    rowPayload.mode = rowPayload.mode || "free"
    rowPayload.inputPrice = hasValidNumber(rowPayload.inputPrice)
        ? Number(rowPayload.inputPrice)
        : hasValidNumber(rowPayload.price)
            ? Number(rowPayload.price)
            : 0
    rowPayload.price = hasValidNumber(rowPayload.price) ? Number(rowPayload.price) : rowPayload.inputPrice
    rowPayload.quantity = hasValidNumber(rowPayload.quantity) ? Number(rowPayload.quantity) : 1
    rowPayload.discountPercent = hasValidNumber(rowPayload.discountPercent) ? Number(rowPayload.discountPercent) : 0
    rowPayload.linkedEntitys = Array.isArray(rowPayload.linkedEntitys) ? rowPayload.linkedEntitys : []

    return rowPayload
}

const normalizeOutgoingDocumentRows = (rows: unknown) => {
    if (!Array.isArray(rows)) return []
    return rows.map((row, index) => normalizeOutgoingDocumentRow(row, index))
}

const buildOutgoingDocumentPayload = (
    args: Record<string, unknown>,
    userId: string,
    tenantId: number,
    includeCreateDefaults = false
) => {
    const payload: Record<string, unknown> = {
        updatedAt: new Date(),
        updatedBy: userId,
    }

    if (includeCreateDefaults) {
        payload.tenant = tenantId
        payload.createdBy = userId
        payload.created_by = userId
        payload.archived = false
        payload.state = stringArg(args, "state") || "Entwurf"
        payload.type = documentTypeArg(args)
        payload.rows = normalizeOutgoingDocumentRows(optionalArrayArg(args, "rows") || [])
    }

    const stringFields = [
        "state",
        "documentDate",
        "deliveryDate",
        "deliveryDateEnd",
        "deliveryDateType",
        "payment_type",
        "title",
        "description",
        "startText",
        "endText",
    ]

    for (const field of stringFields) {
        if (args[field] !== undefined) payload[field] = stringArg(args, field)
    }

    for (const field of ["customer", "contact", "contract", "project", "plant", "letterhead", "createddocument"] as const) {
        if (args[field] !== undefined) payload[field] = numberArg(args, field)
    }

    if (args.costcentre !== undefined) payload.costcentre = stringArg(args, "costcentre")

    for (const field of ["paymentDays"] as const) {
        if (args[field] !== undefined) payload[field] = numberArg(args, field)
    }

    if (args.type !== undefined) payload.type = documentTypeArg(args)
    if (args.address !== undefined) payload.address = optionalObjectArg(args, "address")
    if (args.info !== undefined) payload.info = optionalObjectArg(args, "info")
    if (args.agriculture !== undefined) payload.agriculture = optionalObjectArg(args, "agriculture")
    if (args.report !== undefined) payload.report = optionalObjectArg(args, "report") || {}
    if (args.serialConfig !== undefined) payload.serialConfig = optionalObjectArg(args, "serialConfig") || {}
    if (args.rows !== undefined) payload.rows = normalizeOutgoingDocumentRows(optionalArrayArg(args, "rows") || [])
    if (args.usedAdvanceInvoices !== undefined) payload.usedAdvanceInvoices = optionalArrayArg(args, "usedAdvanceInvoices") || []
    if (typeof args.availableInPortal === "boolean") payload.availableInPortal = args.availableInPortal
    if (typeof args.advanceInvoiceResolved === "boolean") payload.advanceInvoiceResolved = args.advanceInvoiceResolved
    if (args.customSurchargePercentage !== undefined) payload.customSurchargePercentage = numberArg(args, "customSurchargePercentage") || 0

    return payload
}

const assertNoManualDocumentNumber = (args: Record<string, unknown>) => {
    if (args.documentNumber !== undefined || args.assignDocumentNumber !== undefined) {
        throw new Error("Belegnummern werden nur beim Finalisieren vergeben")
    }
}

const incomingInvoiceAccountsArg = (args: Record<string, unknown>) => {
    if (args.accounts === undefined) return undefined
    if (!Array.isArray(args.accounts)) throw new Error("accounts muss ein Array sein")
    return args.accounts
}

const base64BufferArg = (args: Record<string, unknown>, key: string) => {
    const value = stringArg(args, key)
    if (!value) throw new Error(`${key} ist erforderlich`)

    const base64 = value.includes(",") ? value.split(",").pop() || "" : value
    const buffer = Buffer.from(base64, "base64")

    if (!buffer.length) throw new Error(`${key} enthält keine Datei`)
    if (buffer.length > MAX_MCP_UPLOAD_BYTES) throw new Error("Datei ist größer als 20 MB")

    return buffer
}

const loadIncomingInvoiceFileDefaults = async (context: { server: any; tenantId: number }) => {
    const currentYear = new Date().getFullYear()

    const [tag] = await context.server.db
        .select({ id: filetags.id })
        .from(filetags)
        .where(and(
            eq(filetags.tenant, context.tenantId),
            eq(filetags.incomingDocumentType, "invoices"),
            eq(filetags.archived, false)
        ))
        .limit(1)

    const [folder] = await context.server.db
        .select({ id: folders.id })
        .from(folders)
        .where(and(
            eq(folders.tenant, context.tenantId),
            eq(folders.function, "incomingInvoices"),
            eq(folders.year, currentYear),
            eq(folders.archived, false)
        ))
        .limit(1)

    return {
        folderId: folder?.id || null,
        fileTagId: tag?.id || null,
    }
}

const buildIncomingInvoicePayload = (
    args: Record<string, unknown>,
    userId: string,
    tenantId: number,
    includeCreateDefaults = false
) => {
    const payload: Record<string, unknown> = {
        updatedAt: new Date(),
        updatedBy: userId,
    }

    if (includeCreateDefaults) {
        payload.tenant = tenantId
        payload.state = "Entwurf"
        payload.expense = args.expense !== undefined ? args.expense === true : true
        payload.paid = false
        payload.archived = false
    }

    for (const field of ["state", "reference", "date", "dueDate", "description", "paymentType"] as const) {
        if (args[field] !== undefined) payload[field] = stringArg(args, field)
    }
    if (args.vendor !== undefined) payload.vendor = numberArg(args, "vendor")
    if (args.document !== undefined) payload.document = numberArg(args, "document")
    if (args.expense !== undefined) payload.expense = args.expense === true
    if (args.paid !== undefined) payload.paid = args.paid === true

    const accountsPayload = incomingInvoiceAccountsArg(args)
    if (accountsPayload !== undefined) payload.accounts = accountsPayload

    return payload
}

const isDepreciationBookingMode = (value: unknown) =>
    ["depreciation", "depreciation_bundle"].includes(String(value || ""))

const validateIncomingInvoiceData = (invoice: Record<string, any>) => {
    const errors: Array<{ message: string; type: "breaking" | "warning" }> = []

    if (!invoice.vendor) errors.push({ message: "Es ist kein Lieferant ausgewählt", type: "breaking" })
    if (!String(invoice.reference || "").trim()) errors.push({ message: "Es ist keine Referenz angegeben", type: "breaking" })
    if (!invoice.date) errors.push({ message: "Es ist kein Datum ausgewählt", type: "breaking" })
    if (!Array.isArray(invoice.accounts) || invoice.accounts.length === 0) {
        errors.push({ message: "Es ist keine Position vorhanden", type: "breaking" })
    }

    ;(Array.isArray(invoice.accounts) ? invoice.accounts : []).forEach((account: any, idx: number) => {
        const pos = idx + 1

        if (!account?.account) errors.push({ message: `Pos ${pos}: Keine Kategorie`, type: "breaking" })
        if (!hasValidNumber(account?.amountNet) && !hasValidNumber(account?.amountGross)) {
            errors.push({ message: `Pos ${pos}: Kein gültiger Betrag`, type: "breaking" })
        }
        if (!account?.taxType) errors.push({ message: `Pos ${pos}: Kein Steuerschlüssel`, type: "breaking" })
        if (hasValidNumber(account?.amountNet) && !hasValidNumber(account?.amountTax)) {
            errors.push({ message: `Pos ${pos}: Steuerbetrag fehlt, bitte Steuer neu berechnen`, type: "warning" })
        }
        if (isDepreciationBookingMode(account?.bookingMode) && !Number(account?.depreciationMonths)) {
            errors.push({ message: `Pos ${pos}: Abschreibungsdauer fehlt`, type: "breaking" })
        }
        if (account?.bookingMode === "depreciation_bundle" && !String(account?.depreciationGroup || "").trim()) {
            errors.push({ message: `Pos ${pos}: Sammelposten benötigt einen Gruppennamen`, type: "breaking" })
        }
    })

    const order = { breaking: 0, warning: 1 }
    errors.sort((a, b) => order[a.type] - order[b.type])

    return {
        valid: errors.every((error) => error.type !== "breaking"),
        errors,
        blockingErrors: errors.filter((error) => error.type === "breaking"),
        warnings: errors.filter((error) => error.type === "warning"),
    }
}

export const accountingTools: McpTool[] = [
    {
        name: "accounting.outgoing_documents.tax_types.list",
        title: "Steuertypen für Ausgangsbelege auflisten",
        description: "Listet die unterstützten Dokument-Steuertypen für Ausgangsbelege.",
        requiredPermissions: ["accounting.outgoing_documents.read"],
        inputSchema: {
            type: "object",
            properties: {},
        },
        async handler() {
            return {
                rows: [
                    { key: "Standard", label: "Standard", forcesZeroTaxPercent: false },
                    { key: "13b UStG", label: "13b UStG", forcesZeroTaxPercent: true },
                    { key: "19 UStG", label: "19 UStG Kleinunternehmer", forcesZeroTaxPercent: true },
                    { key: "12.3 UStG", label: "12.3 UStG", forcesZeroTaxPercent: true },
                ],
            }
        },
    },
    {
        name: "accounting.outgoing_documents.list",
        title: "Ausgangsbelege auflisten",
        description: "Listet Ausgangsbelege des aktiven Mandanten mit optionalen Filtern.",
        requiredPermissions: ["accounting.outgoing_documents.read"],
        inputSchema: {
            type: "object",
            properties: {
                type: { type: "string", description: "Belegart, z. B. invoices, quotes oder deliveryNotes." },
                state: { type: "string", description: "Optionaler Statusfilter, z. B. Entwurf oder Gebucht." },
                customer: { type: "number" },
                project: { type: "number" },
                includeArchived: { type: "boolean", default: false },
                limit: { type: "number", minimum: 1, maximum: 100 },
            },
        },
        async handler(context, args) {
            const conditions = [eq(createddocuments.tenant, context.tenantId)]
            const type = stringArg(args, "type")
            const state = stringArg(args, "state")
            const customer = numberArg(args, "customer")
            const project = numberArg(args, "project")

            if (type) {
                if (!allowedOutgoingDocumentTypes.has(type)) throw new Error(`Ungültige Belegart: ${type}`)
                conditions.push(eq(createddocuments.type, type))
            }
            if (state) conditions.push(eq(createddocuments.state, state))
            if (customer) conditions.push(eq(createddocuments.customer, customer))
            if (project) conditions.push(eq(createddocuments.project, project))
            if (args.includeArchived !== true) conditions.push(eq(createddocuments.archived, false))

            const rows = await context.server.db
                .select()
                .from(createddocuments)
                .where(and(...conditions))
                .orderBy(desc(createddocuments.createdAt))
                .limit(limitFromArgs(args))

            return { rows }
        },
    },
    {
        name: "accounting.outgoing_documents.get",
        title: "Ausgangsbeleg laden",
        description: "Lädt einen Ausgangsbeleg des aktiven Mandanten anhand seiner ID.",
        requiredPermissions: ["accounting.outgoing_documents.read"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const rows = await context.server.db
                .select()
                .from(createddocuments)
                .where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
                .limit(1)

            if (!rows[0]) throw new Error("Ausgangsbeleg nicht gefunden")
            return { document: rows[0] }
        },
    },
    {
        name: "accounting.outgoing_documents.create",
        title: "Ausgangsbeleg erstellen",
        description: "Erstellt einen Ausgangsbeleg-Entwurf im aktiven Mandanten. Belegnummern werden erst beim Finalisieren vergeben.",
        requiredPermissions: ["accounting.outgoing_documents.write"],
        inputSchema: {
            type: "object",
            required: ["type"],
            properties: {
                type: { type: "string" },
                customer: { type: "number" },
                contact: { type: "number" },
                contract: { type: "number" },
                project: { type: "number" },
                costcentre: { type: "string", description: "Kostenstellen-UUID als Belegvorgabe." },
                plant: { type: "number" },
                documentDate: { type: "string" },
                deliveryDate: { type: "string" },
                deliveryDateEnd: { type: "string" },
                deliveryDateType: { type: "string" },
                paymentDays: { type: "number" },
                payment_type: { type: "string" },
                taxType: {
                    type: "string",
                    enum: ["Standard", "13b UStG", "19 UStG", "12.3 UStG"],
                    description: "Steuertyp des gesamten Ausgangsdokuments, nicht der einzelne USt-Satz einer Position.",
                },
                title: { type: "string" },
                description: { type: "string" },
                startText: { type: "string" },
                endText: { type: "string" },
                address: { type: "object" },
                rows: { type: "array" },
                letterhead: { type: "number" },
                availableInPortal: { type: "boolean" },
                customSurchargePercentage: { type: "number" },
                report: { type: "object" },
            },
        },
        async handler(context, args) {
            assertNoManualDocumentNumber(args)
            const payload = buildOutgoingDocumentPayload(args, context.userId, context.tenantId, true)
            payload.state = "Entwurf"
            applyOutgoingDocumentTaxType(payload, args, String(payload.type))

            const [created] = await context.server.db
                .insert(createddocuments)
                .values(payload as any)
                .returning()

            return { document: created }
        },
    },
    {
        name: "accounting.outgoing_documents.update",
        title: "Ausgangsbeleg aktualisieren",
        description: "Aktualisiert einen Ausgangsbeleg im aktiven Mandanten, solange er noch nicht finalisiert ist.",
        requiredPermissions: ["accounting.outgoing_documents.write"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
                type: { type: "string" },
                state: { type: "string" },
                customer: { type: "number" },
                contact: { type: "number" },
                contract: { type: "number" },
                project: { type: "number" },
                costcentre: { type: "string", description: "Kostenstellen-UUID als Belegvorgabe." },
                plant: { type: "number" },
                documentDate: { type: "string" },
                deliveryDate: { type: "string" },
                deliveryDateEnd: { type: "string" },
                deliveryDateType: { type: "string" },
                paymentDays: { type: "number" },
                payment_type: { type: "string" },
                taxType: {
                    type: "string",
                    enum: ["Standard", "13b UStG", "19 UStG", "12.3 UStG"],
                    description: "Steuertyp des gesamten Ausgangsdokuments, nicht der einzelne USt-Satz einer Position.",
                },
                title: { type: "string" },
                description: { type: "string" },
                startText: { type: "string" },
                endText: { type: "string" },
                address: { type: "object" },
                rows: { type: "array" },
                letterhead: { type: "number" },
                availableInPortal: { type: "boolean" },
                customSurchargePercentage: { type: "number" },
                report: { type: "object" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")
            assertNoManualDocumentNumber(args)

            const [existing] = await context.server.db
                .select()
                .from(createddocuments)
                .where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
                .limit(1)

            if (!existing) throw new Error("Ausgangsbeleg nicht gefunden")
            if (existing.state !== "Entwurf") throw new Error("Finalisierte Ausgangsbelege können nicht über update geändert werden")

            const payload = buildOutgoingDocumentPayload(args, context.userId, context.tenantId)
            payload.state = "Entwurf"
            applyOutgoingDocumentTaxType(payload, args, String(payload.type || existing.type), existing.taxType, existing.rows)

            const [updated] = await context.server.db
                .update(createddocuments)
                .set(payload as any)
                .where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
                .returning()

            return { document: updated }
        },
    },
    {
        name: "accounting.outgoing_documents.finalize",
        title: "Ausgangsbeleg finalisieren",
        description: "Finalisiert einen Ausgangsbeleg, setzt den Status auf Gebucht und vergibt dabei genau einmal eine Belegnummer.",
        requiredPermissions: ["accounting.outgoing_documents.write"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
                documentDate: { type: "string" },
                deliveryDate: { type: "string" },
                deliveryDateEnd: { type: "string" },
                paymentDays: { type: "number" },
                payment_type: { type: "string" },
                taxType: {
                    type: "string",
                    enum: ["Standard", "13b UStG", "19 UStG", "12.3 UStG"],
                    description: "Steuertyp des gesamten Ausgangsdokuments, nicht der einzelne USt-Satz einer Position.",
                },
                rows: { type: "array" },
                report: { type: "object" },
                availableInPortal: { type: "boolean" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")
            assertNoManualDocumentNumber(args)

            const [existing] = await context.server.db
                .select()
                .from(createddocuments)
                .where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
                .limit(1)

            if (!existing) throw new Error("Ausgangsbeleg nicht gefunden")
            if (existing.documentNumber) throw new Error("Ausgangsbeleg wurde bereits finalisiert")

            const payload = buildOutgoingDocumentPayload(args, context.userId, context.tenantId)
            const result = await useNextNumberRangeNumber(context.server, context.tenantId, existing.type)

            payload.state = "Gebucht"
            payload.documentNumber = result.usedNumber
            applyOutgoingDocumentTaxType(payload, args, existing.type, existing.taxType, existing.rows)

            const [updated] = await context.server.db
                .update(createddocuments)
                .set(payload as any)
                .where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
                .returning()

            return { document: updated }
        },
    },
    {
        name: "accounting.outgoing_documents.archive",
        title: "Ausgangsbeleg archivieren",
        description: "Archiviert einen Ausgangsbeleg im aktiven Mandanten.",
        requiredPermissions: ["accounting.outgoing_documents.write"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const [updated] = await context.server.db
                .update(createddocuments)
                .set({
                    archived: true,
                    updatedAt: new Date(),
                    updatedBy: context.userId,
                })
                .where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
                .returning()

            if (!updated) throw new Error("Ausgangsbeleg nicht gefunden")
            return { document: updated }
        },
    },
    {
        name: "accounting.accounts.search",
        title: "Konten suchen",
        description: "Sucht Sachkonten im aktiven Kontenrahmen des Mandanten.",
        requiredPermissions: ["accounting.accounts.read"],
        inputSchema: {
            type: "object",
            properties: {
                query: { type: "string", description: "Suchtext für Kontonummer, Bezeichnung oder Beschreibung." },
                limit: { type: "number", minimum: 1, maximum: 100 },
            },
        },
        async handler(context, args) {
            const tenantRows = await context.server.db.query.tenants.findMany({
                where: (tenant, { eq }) => eq(tenant.id, context.tenantId),
                columns: {
                    accountChart: true,
                },
                limit: 1,
            })
            const accountChart = tenantRows[0]?.accountChart || "skr03"
            const query = stringArg(args, "query")
            const limit = limitFromArgs(args)

            const whereCond = query
                ? and(
                    eq(accounts.accountChart, accountChart),
                    or(
                        ilike(accounts.number, `%${query}%`),
                        ilike(accounts.label, `%${query}%`),
                        ilike(accounts.description, `%${query}%`)
                    )
                )
                : eq(accounts.accountChart, accountChart)

            const rows = await context.server.db
                .select()
                .from(accounts)
                .where(whereCond)
                .orderBy(accounts.number)
                .limit(limit)

            return { accountChart, rows }
        },
    },
    {
        name: "accounting.incoming_invoices.list",
        title: "Eingangsrechnungen auflisten",
        description: "Listet Eingangsrechnungen des aktiven Mandanten.",
        requiredPermissions: ["accounting.incoming_invoices.read"],
        inputSchema: {
            type: "object",
            properties: {
                state: { type: "string", description: "Optionaler Statusfilter." },
                paid: { type: "boolean", description: "Optionaler Zahlungsstatus." },
                includeArchived: { type: "boolean", default: false },
                limit: { type: "number", minimum: 1, maximum: 100 },
            },
        },
        async handler(context, args) {
            const conditions = [eq(incominginvoices.tenant, context.tenantId)]
            const state = stringArg(args, "state")

            if (state) conditions.push(eq(incominginvoices.state, state))
            if (typeof args.paid === "boolean") conditions.push(eq(incominginvoices.paid, args.paid))
            if (args.includeArchived !== true) conditions.push(eq(incominginvoices.archived, false))

            const rows = await context.server.db
                .select()
                .from(incominginvoices)
                .where(and(...conditions))
                .orderBy(desc(incominginvoices.createdAt))
                .limit(limitFromArgs(args))

            return { rows }
        },
    },
    {
        name: "accounting.incoming_invoices.get",
        title: "Eingangsrechnung laden",
        description: "Lädt eine Eingangsrechnung des aktiven Mandanten anhand ihrer ID.",
        requiredPermissions: ["accounting.incoming_invoices.read"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const rows = await context.server.db
                .select()
                .from(incominginvoices)
                .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
                .limit(1)

            if (!rows[0]) throw new Error("Eingangsrechnung nicht gefunden")
            return { invoice: rows[0] }
        },
    },
    {
        name: "accounting.incoming_invoices.files.list",
        title: "Dateien eines Eingangsbelegs auflisten",
        description: "Listet Dateien, die mit einem Eingangsbeleg im aktiven Mandanten verknüpft sind.",
        requiredPermissions: ["accounting.incoming_invoices.read"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const rows = await context.server.db
                .select()
                .from(files)
                .where(and(
                    eq(files.incominginvoice, id),
                    eq(files.tenant, context.tenantId),
                    eq(files.archived, false)
                ))
                .orderBy(desc(files.createdAt))

            return { rows }
        },
    },
    {
        name: "accounting.incoming_invoices.files.upload",
        title: "Datei für Eingangsbeleg hochladen",
        description: "Lädt eine PDF- oder Bilddatei als Base64 hoch, verknüpft sie optional mit einem Eingangsbeleg oder stößt die automatische Vorbereitung an.",
        requiredPermissions: ["accounting.incoming_invoices.write"],
        inputSchema: {
            type: "object",
            required: ["filename", "contentBase64"],
            properties: {
                filename: { type: "string", description: "Dateiname, z. B. rechnung.pdf." },
                contentBase64: { type: "string", description: "Dateiinhalt als Base64 oder Data-URL." },
                mimeType: { type: "string", description: "MIME-Type, z. B. application/pdf oder image/jpeg." },
                invoiceId: { type: "number", description: "Optional: vorhandener Eingangsbeleg, mit dem die Datei verknüpft wird." },
                prepare: {
                    type: "boolean",
                    default: true,
                    description: "Wenn kein invoiceId und keine invoice-Daten übergeben werden, wird nach dem Upload die automatische Eingangsbeleg-Vorbereitung ausgeführt.",
                },
                invoice: {
                    type: "object",
                    description: "Optional: Daten für einen neuen Eingangsbeleg-Entwurf, der direkt mit der Datei verknüpft wird.",
                    properties: {
                        vendor: { type: "number" },
                        reference: { type: "string" },
                        date: { type: "string" },
                        dueDate: { type: "string" },
                        document: { type: "number" },
                        description: { type: "string" },
                        paymentType: { type: "string" },
                        accounts: { type: "array" },
                        expense: { type: "boolean" },
                    },
                },
            },
        },
        async handler(context, args) {
            const filename = stringArg(args, "filename")
            if (!filename) throw new Error("filename ist erforderlich")

            const buffer = base64BufferArg(args, "contentBase64")
            const mimeType = stringArg(args, "mimeType") || "application/octet-stream"
            const invoiceId = numberArg(args, "invoiceId")
            const invoiceArgs = optionalObjectArg(args, "invoice") as Record<string, unknown> | null
            const shouldPrepare = args.prepare !== false && !invoiceId && !invoiceArgs

            let linkedInvoice: any = null

            if (invoiceId) {
                const [existing] = await context.server.db
                    .select()
                    .from(incominginvoices)
                    .where(and(eq(incominginvoices.id, invoiceId), eq(incominginvoices.tenant, context.tenantId)))
                    .limit(1)

                if (!existing) throw new Error("Eingangsbeleg nicht gefunden")
                linkedInvoice = existing
            } else if (invoiceArgs) {
                const payload = buildIncomingInvoicePayload(invoiceArgs, context.userId, context.tenantId, true)
                payload.state = "Entwurf"

                const [createdInvoice] = await context.server.db
                    .insert(incominginvoices)
                    .values(payload as any)
                    .returning()

                linkedInvoice = createdInvoice
            }

            const defaults = await loadIncomingInvoiceFileDefaults(context)
            const saved = await saveFile(
                context.server,
                context.tenantId,
                null,
                {
                    filename,
                    content: buffer,
                    contentType: mimeType,
                },
                defaults.folderId,
                defaults.fileTagId,
                {
                    incominginvoice: linkedInvoice?.id || null,
                    createdBy: context.userId,
                    updatedBy: context.userId,
                    updatedAt: new Date(),
                }
            )

            if (!saved) throw new Error("Datei konnte nicht gespeichert werden")

            if (shouldPrepare) {
                await context.server.services.prepareIncomingInvoices.run(context.tenantId)
            }

            const [file] = await context.server.db
                .select()
                .from(files)
                .where(and(eq(files.id, saved.id), eq(files.tenant, context.tenantId)))
                .limit(1)

            if (file?.incominginvoice && !linkedInvoice) {
                const [preparedInvoice] = await context.server.db
                    .select()
                    .from(incominginvoices)
                    .where(and(eq(incominginvoices.id, file.incominginvoice), eq(incominginvoices.tenant, context.tenantId)))
                    .limit(1)

                linkedInvoice = preparedInvoice || null
            }

            return {
                file,
                invoice: linkedInvoice,
                validation: linkedInvoice ? validateIncomingInvoiceData(linkedInvoice as Record<string, any>) : null,
                prepared: Boolean(shouldPrepare && linkedInvoice),
            }
        },
    },
    {
        name: "accounting.incoming_invoices.validate",
        title: "Eingangsbeleg validieren",
        description: "Prüft einen Eingangsbeleg mit denselben Pflichtregeln wie die FEDEO-Oberfläche vor dem Buchen.",
        requiredPermissions: ["accounting.incoming_invoices.read"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const rows = await context.server.db
                .select()
                .from(incominginvoices)
                .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
                .limit(1)

            if (!rows[0]) throw new Error("Eingangsbeleg nicht gefunden")
            return validateIncomingInvoiceData(rows[0] as Record<string, any>)
        },
    },
    {
        name: "accounting.incoming_invoices.create",
        title: "Eingangsbeleg erstellen",
        description: "Erstellt einen Eingangsbeleg-Entwurf im aktiven Mandanten.",
        requiredPermissions: ["accounting.incoming_invoices.write"],
        inputSchema: {
            type: "object",
            properties: {
                vendor: { type: "number" },
                reference: { type: "string" },
                date: { type: "string" },
                dueDate: { type: "string" },
                document: { type: "number" },
                description: { type: "string" },
                paymentType: { type: "string" },
                accounts: { type: "array" },
                expense: { type: "boolean" },
            },
        },
        async handler(context, args) {
            const payload = buildIncomingInvoicePayload(args, context.userId, context.tenantId, true)
            payload.state = "Entwurf"

            const [created] = await context.server.db
                .insert(incominginvoices)
                .values(payload as any)
                .returning()

            return { invoice: created, validation: validateIncomingInvoiceData(created as Record<string, any>) }
        },
    },
    {
        name: "accounting.incoming_invoices.update",
        title: "Eingangsbeleg bearbeiten",
        description: "Bearbeitet einen noch nicht gebuchten Eingangsbeleg im aktiven Mandanten.",
        requiredPermissions: ["accounting.incoming_invoices.write"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
                vendor: { type: "number" },
                reference: { type: "string" },
                date: { type: "string" },
                dueDate: { type: "string" },
                document: { type: "number" },
                description: { type: "string" },
                paymentType: { type: "string" },
                accounts: { type: "array" },
                expense: { type: "boolean" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const [existing] = await context.server.db
                .select()
                .from(incominginvoices)
                .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
                .limit(1)

            if (!existing) throw new Error("Eingangsbeleg nicht gefunden")
            if (existing.state === "Gebucht") throw new Error("Gebuchte Eingangsbelege können nicht über update geändert werden")

            const payload = buildIncomingInvoicePayload(args, context.userId, context.tenantId)
            payload.state = existing.state || "Entwurf"

            const [updated] = await context.server.db
                .update(incominginvoices)
                .set(payload as any)
                .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
                .returning()

            return { invoice: updated, validation: validateIncomingInvoiceData(updated as Record<string, any>) }
        },
    },
    {
        name: "accounting.incoming_invoices.book",
        title: "Eingangsbeleg buchen",
        description: "Validiert und bucht einen vorbereiteten oder als Entwurf gespeicherten Eingangsbeleg.",
        requiredPermissions: ["accounting.incoming_invoices.write"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const [existing] = await context.server.db
                .select()
                .from(incominginvoices)
                .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
                .limit(1)

            if (!existing) throw new Error("Eingangsbeleg nicht gefunden")
            if (existing.state === "Gebucht") return { invoice: existing, validation: validateIncomingInvoiceData(existing as Record<string, any>) }

            const validation = validateIncomingInvoiceData(existing as Record<string, any>)
            if (!validation.valid) {
                return {
                    booked: false,
                    invoice: existing,
                    validation,
                }
            }

            const [updated] = await context.server.db
                .update(incominginvoices)
                .set({
                    state: "Gebucht",
                    updatedAt: new Date(),
                    updatedBy: context.userId,
                })
                .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
                .returning()

            return {
                booked: true,
                invoice: updated,
                validation: validateIncomingInvoiceData(updated as Record<string, any>),
            }
        },
    },
    {
        name: "accounting.incoming_invoices.archive",
        title: "Eingangsbeleg archivieren",
        description: "Archiviert einen Eingangsbeleg im aktiven Mandanten.",
        requiredPermissions: ["accounting.incoming_invoices.write"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const [updated] = await context.server.db
                .update(incominginvoices)
                .set({
                    archived: true,
                    updatedAt: new Date(),
                    updatedBy: context.userId,
                })
                .where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
                .returning()

            if (!updated) throw new Error("Eingangsbeleg nicht gefunden")
            return { invoice: updated }
        },
    },
    {
        name: "accounting.bank_statements.list",
        title: "Bankumsätze auflisten",
        description: "Listet Bankumsätze des aktiven Mandanten.",
        requiredPermissions: ["accounting.bank.read"],
        inputSchema: {
            type: "object",
            properties: {
                account: { type: "number", description: "Optionale Bankkonto-ID." },
                includeArchived: { type: "boolean", default: false },
                limit: { type: "number", minimum: 1, maximum: 100 },
            },
        },
        async handler(context, args) {
            const conditions = [eq(bankstatements.tenant, context.tenantId)]
            const account = numberArg(args, "account")

            if (account) conditions.push(eq(bankstatements.account, account))
            if (args.includeArchived !== true) conditions.push(eq(bankstatements.archived, false))

            const rows = await context.server.db
                .select()
                .from(bankstatements)
                .where(and(...conditions))
                .orderBy(desc(bankstatements.date), desc(bankstatements.id))
                .limit(limitFromArgs(args))

            return { rows }
        },
    },
    {
        name: "accounting.bank_statements.get",
        title: "Bankumsatz laden",
        description: "Lädt einen Bankumsatz des aktiven Mandanten anhand seiner ID.",
        requiredPermissions: ["accounting.bank.read"],
        inputSchema: {
            type: "object",
            required: ["id"],
            properties: {
                id: { type: "number" },
            },
        },
        async handler(context, args) {
            const id = numberArg(args, "id")
            if (!id) throw new Error("id ist erforderlich")

            const rows = await context.server.db
                .select()
                .from(bankstatements)
                .where(and(eq(bankstatements.id, id), eq(bankstatements.tenant, context.tenantId)))
                .limit(1)

            if (!rows[0]) throw new Error("Bankumsatz nicht gefunden")
            return { bankStatement: rows[0] }
        },
    },
    {
        name: "accounting.statement_allocations.list",
        title: "Buchungszuordnungen auflisten",
        description: "Listet Buchungszuordnungen des aktiven Mandanten.",
        requiredPermissions: ["accounting.statement_allocations.read"],
        inputSchema: {
            type: "object",
            properties: {
                bankstatement: { type: "number" },
                incominginvoice: { type: "number" },
                includeArchived: { type: "boolean", default: false },
                limit: { type: "number", minimum: 1, maximum: 100 },
            },
        },
        async handler(context, args) {
            const conditions = [eq(statementallocations.tenant, context.tenantId)]
            const bankstatement = numberArg(args, "bankstatement")
            const incominginvoice = numberArg(args, "incominginvoice")

            if (bankstatement) conditions.push(eq(statementallocations.bankstatement, bankstatement))
            if (incominginvoice) conditions.push(eq(statementallocations.incominginvoice, incominginvoice))
            if (args.includeArchived !== true) conditions.push(eq(statementallocations.archived, false))

            const rows = await context.server.db
                .select()
                .from(statementallocations)
                .where(and(...conditions))
                .orderBy(desc(statementallocations.created_at))
                .limit(limitFromArgs(args))

            return { rows }
        },
    },
]
