import { and, eq } from "drizzle-orm";
import * as schema from "../../db/schema";
import { FastifyInstance } from "fastify";

type CompositionRow = {
    product?: number | string | null;
    service?: number | string | null;
    hourrate?: string | null;
    quantity?: number | string | null;
    price?: number | string | null;
    purchasePrice?: number | string | null;
    [key: string]: any;
};

function toNumber(value: any): number {
    const num = Number(value ?? 0);
    return Number.isFinite(num) ? num : 0;
}

function round2(value: number): number {
    return Number(value.toFixed(2));
}

function getJsonNumber(source: unknown, key: string): number {
    if (!source || typeof source !== "object") return 0;
    return toNumber((source as Record<string, unknown>)[key]);
}

function normalizeId(value: unknown): number | null {
    if (value === null || value === undefined || value === "") return null;
    const num = Number(value);
    return Number.isFinite(num) ? num : null;
}

function normalizeUuid(value: unknown): string | null {
    if (typeof value !== "string") return null;
    const trimmed = value.trim();
    return trimmed.length ? trimmed : null;
}

function sanitizeCompositionRows(value: unknown): CompositionRow[] {
    if (!Array.isArray(value)) return [];
    return value.filter((entry): entry is CompositionRow => !!entry && typeof entry === "object");
}

export async function recalculateServicePricesForTenant(server: FastifyInstance, tenantId: number, updatedBy?: string | null) {
    const [services, products, hourrates] = await Promise.all([
        server.db.select().from(schema.services).where(eq(schema.services.tenant, tenantId)),
        server.db.select().from(schema.products).where(eq(schema.products.tenant, tenantId)),
        server.db.select().from(schema.hourrates).where(eq(schema.hourrates.tenant, tenantId)),
    ]);

    const serviceMap = new Map(services.map((item) => [item.id, item]));
    const productMap = new Map(products.map((item) => [item.id, item]));
    const hourrateMap = new Map(hourrates.map((item) => [item.id, item]));

    const memo = new Map<number, {
        sellingTotal: number;
        purchaseTotal: number;
        materialTotal: number;
        materialPurchaseTotal: number;
        workerTotal: number;
        workerPurchaseTotal: number;
        materialComposition: CompositionRow[];
        personalComposition: CompositionRow[];
    }>();
    const stack = new Set<number>();

    const calculateService = (serviceId: number) => {
        if (memo.has(serviceId)) return memo.get(serviceId)!;

        const service = serviceMap.get(serviceId);
        const emptyResult = {
            sellingTotal: 0,
            purchaseTotal: 0,
            materialTotal: 0,
            materialPurchaseTotal: 0,
            workerTotal: 0,
            workerPurchaseTotal: 0,
            materialComposition: [],
            personalComposition: [],
        };

        if (!service) return emptyResult;
        if (stack.has(serviceId)) return emptyResult;

        // Gesperrte Leistungen bleiben bei automatischen Preis-Updates unverändert.
        if (service.priceUpdateLocked) {
            const lockedResult = {
                sellingTotal: getJsonNumber(service.sellingPriceComposed, "total") || toNumber(service.sellingPrice),
                purchaseTotal: getJsonNumber(service.purchasePriceComposed, "total"),
                materialTotal: getJsonNumber(service.sellingPriceComposed, "material"),
                materialPurchaseTotal: getJsonNumber(service.purchasePriceComposed, "material"),
                workerTotal: getJsonNumber(service.sellingPriceComposed, "worker"),
                workerPurchaseTotal: getJsonNumber(service.purchasePriceComposed, "worker"),
                materialComposition: sanitizeCompositionRows(service.materialComposition),
                personalComposition: sanitizeCompositionRows(service.personalComposition),
            };
            memo.set(serviceId, lockedResult);
            return lockedResult;
        }

        stack.add(serviceId);
        try {
            const materialComposition = sanitizeCompositionRows(service.materialComposition);
            const personalComposition = sanitizeCompositionRows(service.personalComposition);
            const hasMaterialComposition = materialComposition.length > 0;
            const hasPersonalComposition = personalComposition.length > 0;

            // Ohne Zusammensetzung keine automatische Überschreibung:
            // manuell gepflegte Preise sollen erhalten bleiben.
            if (!hasMaterialComposition && !hasPersonalComposition) {
                const manualResult = {
                    sellingTotal: getJsonNumber(service.sellingPriceComposed, "total") || toNumber(service.sellingPrice),
                    purchaseTotal: getJsonNumber(service.purchasePriceComposed, "total"),
                    materialTotal: getJsonNumber(service.sellingPriceComposed, "material"),
                    materialPurchaseTotal: getJsonNumber(service.purchasePriceComposed, "material"),
                    workerTotal: getJsonNumber(service.sellingPriceComposed, "worker"),
                    workerPurchaseTotal: getJsonNumber(service.purchasePriceComposed, "worker"),
                    materialComposition,
                    personalComposition,
                };
                memo.set(serviceId, manualResult);
                return manualResult;
            }

            let materialTotal = 0;
            let materialPurchaseTotal = 0;

            const normalizedMaterialComposition = materialComposition.map((entry) => {
                const quantity = toNumber(entry.quantity);
                const productId = normalizeId(entry.product);
                const childServiceId = normalizeId(entry.service);

                let sellingPrice = toNumber(entry.price);
                let purchasePrice = toNumber(entry.purchasePrice);

                if (productId) {
                    const product = productMap.get(productId);
                    sellingPrice = toNumber(product?.selling_price);
                    purchasePrice = toNumber(product?.purchase_price);
                } else if (childServiceId) {
                    const child = calculateService(childServiceId);
                    sellingPrice = toNumber(child.sellingTotal);
                    purchasePrice = toNumber(child.purchaseTotal);
                }

                materialTotal += quantity * sellingPrice;
                materialPurchaseTotal += quantity * purchasePrice;

                return {
                    ...entry,
                    price: round2(sellingPrice),
                    purchasePrice: round2(purchasePrice),
                };
            });

            let workerTotal = 0;
            let workerPurchaseTotal = 0;
            const normalizedPersonalComposition = personalComposition.map((entry) => {
                const quantity = toNumber(entry.quantity);
                const hourrateId = normalizeUuid(entry.hourrate);

                let sellingPrice = toNumber(entry.price);
                let purchasePrice = toNumber(entry.purchasePrice);

                if (hourrateId) {
                    const hourrate = hourrateMap.get(hourrateId);
                    if (hourrate) {
                        sellingPrice = toNumber(hourrate.sellingPrice);
                        purchasePrice = toNumber(hourrate.purchase_price);
                    }
                }

                workerTotal += quantity * sellingPrice;
                workerPurchaseTotal += quantity * purchasePrice;

                return {
                    ...entry,
                    price: round2(sellingPrice),
                    purchasePrice: round2(purchasePrice),
                };
            });

            const result = {
                sellingTotal: round2(materialTotal + workerTotal),
                purchaseTotal: round2(materialPurchaseTotal + workerPurchaseTotal),
                materialTotal: round2(materialTotal),
                materialPurchaseTotal: round2(materialPurchaseTotal),
                workerTotal: round2(workerTotal),
                workerPurchaseTotal: round2(workerPurchaseTotal),
                materialComposition: normalizedMaterialComposition,
                personalComposition: normalizedPersonalComposition,
            };

            memo.set(serviceId, result);
            return result;
        } finally {
            stack.delete(serviceId);
        }
    };

    for (const service of services) {
        calculateService(service.id);
    }

    const updates = services
        .filter((service) => !service.priceUpdateLocked)
        .map(async (service) => {
            const calc = memo.get(service.id);
            if (!calc) return;

            const sellingPriceComposed = {
                worker: calc.workerTotal,
                material: calc.materialTotal,
                total: calc.sellingTotal,
            };

            const purchasePriceComposed = {
                worker: calc.workerPurchaseTotal,
                material: calc.materialPurchaseTotal,
                total: calc.purchaseTotal,
            };

            const unchanged =
                JSON.stringify(service.materialComposition ?? []) === JSON.stringify(calc.materialComposition) &&
                JSON.stringify(service.personalComposition ?? []) === JSON.stringify(calc.personalComposition) &&
                JSON.stringify(service.sellingPriceComposed ?? {}) === JSON.stringify(sellingPriceComposed) &&
                JSON.stringify(service.purchasePriceComposed ?? {}) === JSON.stringify(purchasePriceComposed) &&
                round2(toNumber(service.sellingPrice)) === calc.sellingTotal;

            if (unchanged) return;

            await server.db
                .update(schema.services)
                .set({
                    materialComposition: calc.materialComposition,
                    personalComposition: calc.personalComposition,
                    sellingPriceComposed,
                    purchasePriceComposed,
                    sellingPrice: calc.sellingTotal,
                    updatedAt: new Date(),
                    updatedBy: updatedBy ?? null,
                })
                .where(and(eq(schema.services.id, service.id), eq(schema.services.tenant, tenantId)));
        });

    await Promise.all(updates);
}
