import xmlbuilder from "xmlbuilder";
import { randomUUID } from "node:crypto";
import dayjs from "dayjs";
import { and, eq, inArray } from "drizzle-orm";
import {
    bankaccounts,
    createddocuments,
    customers,
    entitybankaccounts,
    outgoingsepamandates,
    tenants,
} from "../../../db/schema";
import { decrypt } from "../crypt";

const getCreatedDocumentTotal = (item: any) => {
    let totalNet = 0;
    let total19 = 0;
    let total7 = 0;

    const rows = Array.isArray(item.rows) ? item.rows : [];
    rows.forEach((row: any) => {
        if (!["pagebreak", "title", "text"].includes(row.mode)) {
            const rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100)).toFixed(3);
            totalNet += Number(rowPrice);

            if (Number(row.taxPercent) === 19) {
                total19 += Number(rowPrice) * 0.19;
            } else if (Number(row.taxPercent) === 7) {
                total7 += Number(rowPrice) * 0.07;
            }
        }
    });

    return Number((Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))).toFixed(2));
};

const cleanIban = (value: string | null | undefined) => (value || "").replace(/\s+/g, "").toUpperCase();
const cleanBic = (value: string | null | undefined) => (value || "").replace(/\s+/g, "").toUpperCase();

const formatAmount = (value: number) => value.toFixed(2);

const sanitizeText = (value: string | null | undefined, maxLength = 140) => {
    return (value || "")
        .replace(/[\n\r;]/g, " ")
        .replace(/\s+/g, " ")
        .trim()
        .slice(0, maxLength);
};

const getDecryptedEntityBankAccount = (row: typeof entitybankaccounts.$inferSelect) => ({
    iban: cleanIban(decrypt(row.ibanEncrypted as any)),
    bic: cleanBic(decrypt(row.bicEncrypted as any)),
    bankName: decrypt(row.bankNameEncrypted as any),
});

const buildDirectDebitTransaction = (item: any) => {
    const amount = getCreatedDocumentTotal(item.document);
    if (amount <= 0) {
        throw new Error(`Beleg ${item.document.documentNumber || item.document.id} hat keinen positiven Zahlungsbetrag.`);
    }

    const debtorBankAccount = getDecryptedEntityBankAccount(item.debtorBankAccount);
    if (!debtorBankAccount.iban || !debtorBankAccount.bic) {
        throw new Error(`Bankverbindung für Mandat ${item.mandate.reference} ist unvollständig.`);
    }

    if (!item.mandate.signedAt) {
        throw new Error(`Mandat ${item.mandate.reference} hat kein Unterschriftsdatum.`);
    }

    return {
        amount,
        xml: {
            PmtId: {
                EndToEndId: sanitizeText(item.document.documentNumber || `Beleg-${item.document.id}`, 35),
            },
            InstdAmt: {
                "@Ccy": "EUR",
                "#text": formatAmount(amount),
            },
            DrctDbtTx: {
                MndtRltdInf: {
                    MndtId: sanitizeText(item.mandate.reference, 35),
                    DtOfSgntr: dayjs(item.mandate.signedAt).format("YYYY-MM-DD"),
                    AmdmntInd: "false",
                },
            },
            DbtrAgt: {
                FinInstnId: {
                    BIC: debtorBankAccount.bic,
                },
            },
            Dbtr: {
                Nm: sanitizeText(item.customer.name, 70),
            },
            DbtrAcct: {
                Id: {
                    IBAN: debtorBankAccount.iban,
                },
            },
            RmtInf: {
                Ustrd: sanitizeText(`Rechnung ${item.document.documentNumber || item.document.id}`),
            },
        },
    };
};

export const createSEPAExport = async (
    server: any,
    idsToExport: number[],
    tenantId: number,
    creditorBankaccountId: number
) => {
    if (!idsToExport.length) {
        throw new Error("Es wurden keine Belege für den SEPA-Export ausgewählt.");
    }

    const [tenantData] = await server.db
        .select()
        .from(tenants)
        .where(eq(tenants.id, tenantId))
        .limit(1);

    if (!tenantData?.creditorId) {
        throw new Error("Für den Mandanten ist keine Gläubiger-ID hinterlegt.");
    }

    const [creditorBankAccount] = await server.db
        .select()
        .from(bankaccounts)
        .where(and(
            eq(bankaccounts.id, creditorBankaccountId),
            eq(bankaccounts.tenant, tenantId)
        ))
        .limit(1);

    if (!creditorBankAccount) {
        throw new Error("Das ausgewählte Gläubigerkonto wurde nicht gefunden.");
    }

    const rows = await server.db
        .select({
            document: createddocuments,
            customer: customers,
            mandate: outgoingsepamandates,
            debtorBankAccount: entitybankaccounts,
        })
        .from(createddocuments)
        .innerJoin(customers, eq(createddocuments.customer, customers.id))
        .innerJoin(outgoingsepamandates, eq(createddocuments.outgoingsepamandate, outgoingsepamandates.id))
        .innerJoin(entitybankaccounts, eq(outgoingsepamandates.bankaccount, entitybankaccounts.id))
        .where(and(
            eq(createddocuments.tenant, tenantId),
            eq(createddocuments.payment_type, "direct-debit"),
            inArray(createddocuments.id, idsToExport)
        ));

    if (rows.length !== idsToExport.length) {
        throw new Error("Nicht alle ausgewählten Belege sind gültige SEPA-Lastschrift-Belege mit Mandat.");
    }

    const invalidMandate = rows.find((row) => row.mandate.tenant !== tenantId || row.mandate.status !== "Aktiv" || row.mandate.archived);
    if (invalidMandate) {
        throw new Error(`Mandat ${invalidMandate.mandate.reference} ist nicht aktiv oder gehört nicht zum Mandanten.`);
    }

    const transactions = rows.map((row) => ({
        ...row,
        transaction: buildDirectDebitTransaction(row),
    }));

    const totalAmount = Number(transactions.reduce((sum, item) => sum + item.transaction.amount, 0).toFixed(2));
    const messageId = randomUUID();
    const collectionDate = dayjs().add(3, "days").format("YYYY-MM-DD");
    const createdAt = dayjs().format("YYYY-MM-DDTHH:mm:ss");
    const creditorIban = cleanIban(creditorBankAccount.iban);
    const creditorBic = cleanBic(creditorBankAccount.bankId);

    type SepaTransaction = (typeof transactions)[number];
    const groupedTransactions: Record<string, SepaTransaction[]> = {};

    transactions.forEach((item) => {
        const key = `${item.mandate.mandateType || "CORE"}-${item.mandate.sequenceType || "RCUR"}`;
        groupedTransactions[key] = groupedTransactions[key] || [];
        groupedTransactions[key].push(item);
    });

    const paymentInformations = Object.entries(groupedTransactions).map(([key, items], index) => {
        const groupTotal = Number(items.reduce((sum, item) => sum + item.transaction.amount, 0).toFixed(2));
        const [mandateType, sequenceType] = key.split("-");

        return {
            PmtInfId: sanitizeText(`${messageId}-${index + 1}`, 35),
            PmtMtd: "DD",
            BtchBookg: "true",
            NbOfTxs: items.length,
            CtrlSum: formatAmount(groupTotal),
            PmtTpInf: {
                SvcLvl: {
                    Cd: "SEPA",
                },
                LclInstrm: {
                    Cd: mandateType,
                },
                SeqTp: sequenceType,
            },
            ReqdColltnDt: collectionDate,
            Cdtr: {
                Nm: sanitizeText(tenantData.name, 70),
            },
            CdtrAcct: {
                Id: {
                    IBAN: creditorIban,
                },
            },
            CdtrAgt: {
                FinInstnId: {
                    BIC: creditorBic,
                },
            },
            ChrgBr: "SLEV",
            CdtrSchmeId: {
                Id: {
                    PrvtId: {
                        Othr: {
                            Id: tenantData.creditorId,
                            SchmeNm: {
                                Prtry: "SEPA",
                            },
                        },
                    },
                },
            },
            DrctDbtTxInf: items.map((item) => item.transaction.xml),
        };
    });

    const obj = {
        Document: {
            "@xmlns": "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02",
            CstmrDrctDbtInitn: {
                GrpHdr: {
                    MsgId: sanitizeText(messageId, 35),
                    CreDtTm: createdAt,
                    NbOfTxs: transactions.length,
                    CtrlSum: formatAmount(totalAmount),
                    InitgPty: {
                        Nm: sanitizeText(tenantData.name, 70),
                    },
                },
                PmtInf: paymentInformations,
            },
        },
    };

    const xml = xmlbuilder.create(obj, { encoding: "UTF-8", standalone: true }).end({ pretty: true });
    const documentDates = rows.map((row) => dayjs(row.document.documentDate)).filter((date) => date.isValid());
    const startDate = documentDates.reduce((min, date) => date.isBefore(min) ? date : min, documentDates[0] || dayjs());
    const endDate = documentDates.reduce((max, date) => date.isAfter(max) ? date : max, documentDates[0] || dayjs());

    return {
        buffer: Buffer.from(xml, "utf-8"),
        startDate: startDate.toDate(),
        endDate: endDate.toDate(),
        count: transactions.length,
        totalAmount,
    };
};
