import { prisma } from "@/core/prisma";
import { AppError } from "@/core/AppError";
import { DiscountType } from "@prisma/client";
import type { CreatePromoCodeInput, UpdatePromoCodeInput } from "ttm-shared";

/**
 * Ordre déterministe d'application des remises :
 *  1. FREE_PRODUCT  — retire des unités de la base de calcul
 *  2. PERCENTAGE    — pourcentage sur le brut éligible, plafonné au reste
 *  3. FIXED_AMOUNT  — rabais absolu, plafonné au reste
 */
const DISCOUNT_TYPE_PRIORITY: Record<string, number> = {
    FREE_PRODUCT: 0,
    PERCENTAGE: 1,
    FIXED_AMOUNT: 2,
};

function serializePromoCode(pc: any) {
    return {
        id: pc.id,
        shopId: pc.shopId,
        code: pc.code,
        label: pc.label ?? null,
        discountType: pc.discountType,
        discountValue: pc.discountValue,
        targetProductId: pc.targetProductId ?? null,
        targetProductName: pc.targetProduct?.name ?? null,
        targetCategoryId: pc.targetCategoryId ?? null,
        targetCategoryName: pc.targetCategory?.name ?? null,
        maxUses: pc.maxUses ?? null,
        maxUsesPerOrder: pc.maxUsesPerOrder,
        currentUses: pc.currentUses,
        startsAt: pc.startsAt?.toISOString() ?? null,
        expiresAt: pc.expiresAt?.toISOString() ?? null,
        active: pc.active,
        createdAt: pc.createdAt.toISOString(),
    };
}

const PROMO_INCLUDE = {
    targetProduct: { select: { name: true } },
    targetCategory: { select: { name: true } },
};

export const promoCodeService = {
    // ── CRUD ──────────────────────────────────────────────────────────

    async create(shopId: string, input: CreatePromoCodeInput) {
        // Vérifier unicité du code dans le shop
        const existing = await prisma.promoCode.findUnique({
            where: { shopId_code: { shopId, code: input.code.toUpperCase() } },
        });
        if (existing) {
            throw new AppError("Un code promo avec ce code existe déjà", 409);
        }

        // Valider les cibles
        if (input.targetProductId) {
            const product = await prisma.product.findFirst({
                where: { id: input.targetProductId, shopId },
            });
            if (!product) throw new AppError("Produit cible introuvable", 404);
        }
        if (input.targetCategoryId) {
            const cat = await prisma.category.findFirst({
                where: { id: input.targetCategoryId, shopId },
            });
            if (!cat) throw new AppError("Catégorie cible introuvable", 404);
        }

        // Validation cohérence discountType / discountValue
        if (input.discountType === "PERCENTAGE" && (input.discountValue <= 0 || input.discountValue > 10000)) {
            throw new AppError("Le pourcentage doit être entre 0.01% et 100% (1–10000)", 400);
        }
        if (input.discountType === "FREE_PRODUCT" && !input.targetProductId) {
            throw new AppError("FREE_PRODUCT requiert un targetProductId", 400);
        }

        const pc = await prisma.promoCode.create({
            data: {
                shopId,
                code: input.code.toUpperCase(),
                label: input.label ?? null,
                discountType: input.discountType as DiscountType,
                discountValue: input.discountValue,
                targetProductId: input.targetProductId ?? null,
                targetCategoryId: input.targetCategoryId ?? null,
                maxUses: input.maxUses ?? null,
                maxUsesPerOrder: input.maxUsesPerOrder ?? 1,
                startsAt: input.startsAt ? new Date(input.startsAt) : null,
                expiresAt: input.expiresAt ? new Date(input.expiresAt) : null,
            },
            include: PROMO_INCLUDE,
        });

        return serializePromoCode(pc);
    },

    async findAll(shopId: string) {
        const codes = await prisma.promoCode.findMany({
            where: { shopId },
            include: PROMO_INCLUDE,
            orderBy: { createdAt: "desc" },
        });
        return codes.map(serializePromoCode);
    },

    async findById(shopId: string, id: string) {
        const pc = await prisma.promoCode.findFirst({
            where: { id, shopId },
            include: PROMO_INCLUDE,
        });
        if (!pc) return null;
        return serializePromoCode(pc);
    },

    async update(shopId: string, id: string, input: UpdatePromoCodeInput) {
        const pc = await prisma.promoCode.findFirst({ where: { id, shopId } });
        if (!pc) throw new AppError("Code promo introuvable", 404);

        const updated = await prisma.promoCode.update({
            where: { id },
            data: {
                ...(input.label !== undefined ? { label: input.label } : {}),
                ...(input.discountValue !== undefined ? { discountValue: input.discountValue } : {}),
                ...(input.maxUses !== undefined ? { maxUses: input.maxUses } : {}),
                ...(input.maxUsesPerOrder !== undefined ? { maxUsesPerOrder: input.maxUsesPerOrder } : {}),
                ...(input.startsAt !== undefined ? { startsAt: input.startsAt ? new Date(input.startsAt) : null } : {}),
                ...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt ? new Date(input.expiresAt) : null } : {}),
                ...(input.active !== undefined ? { active: input.active } : {}),
            },
            include: PROMO_INCLUDE,
        });

        return serializePromoCode(updated);
    },

    async deactivate(shopId: string, id: string) {
        const pc = await prisma.promoCode.findFirst({ where: { id, shopId } });
        if (!pc) throw new AppError("Code promo introuvable", 404);

        await prisma.promoCode.update({
            where: { id },
            data: { active: false },
        });

        return { success: true };
    },

    // ── APPLICATION SUR UNE COMMANDE ──────────────────────────────────

    async applyToOrder(shopId: string, orderId: string, rawCode: string) {
        const code = rawCode.trim().toUpperCase();

        // 1. Chercher le code promo
        const promo = await prisma.promoCode.findUnique({
            where: { shopId_code: { shopId, code } },
            include: { targetProduct: true, targetCategory: true },
        });

        if (!promo) throw new AppError("Code promo introuvable", 404);
        if (!promo.active) throw new AppError("Ce code promo est désactivé", 400);

        const now = new Date();
        if (promo.startsAt && now < promo.startsAt) {
            throw new AppError("Ce code promo n'est pas encore actif", 400);
        }
        if (promo.expiresAt && now > promo.expiresAt) {
            throw new AppError("Ce code promo a expiré", 400);
        }
        if (promo.maxUses && promo.currentUses >= promo.maxUses) {
            throw new AppError("Ce code promo a atteint sa limite d'utilisation", 400);
        }

        // 2. Charger la commande
        const order = await prisma.order.findFirst({
            where: { id: orderId, shopId },
            include: {
                orderItems: {
                    where: { status: { not: "CANCELLED" } },
                    include: { product: { include: { category: true } } },
                },
                promoCodeUsages: true,
            },
        });

        if (!order) throw new AppError("Commande introuvable", 404);
        if (order.status === "PAID") throw new AppError("Impossible de modifier une commande payée", 400);
        if (order.status === "CANCELLED") throw new AppError("Impossible de modifier une commande annulée", 400);

        // 3. Vérifier que le code n'est pas déjà appliqué trop de fois sur cette commande
        const usagesOnThisOrder = order.promoCodeUsages.filter(u => u.promoCodeId === promo.id);
        if (usagesOnThisOrder.length >= promo.maxUsesPerOrder) {
            throw new AppError(
                `Ce code promo est limité à ${promo.maxUsesPerOrder} utilisation(s) par commande`,
                400,
            );
        }

        // 4. Vérifier que le code apportera bien une remise (dry-run rapide)
        const eligibleItems = this._getEligibleItems(order.orderItems, promo);

        if (eligibleItems.length === 0) {
            throw new AppError("Aucun produit éligible pour ce code promo dans la commande", 400);
        }

        // 4b. Vérifier que le nombre d'usages ne dépasse pas la quantité d'items éligibles
        const eligibleQty = eligibleItems.reduce((sum: number, i: any) => sum + i.quantity, 0);
        if (usagesOnThisOrder.length >= eligibleQty) {
            throw new AppError(
                `Ce code promo a déjà été appliqué pour tous les produits éligibles (${eligibleQty} unité(s))`,
                400,
            );
        }

        // 5. Vérifier que la commande n'est pas déjà intégralement couverte
        const subtotal = order.orderItems.reduce(
            (sum, i) => sum + i.unitPrice * i.quantity, 0,
        );
        const currentDiscount = order.discountTotal;
        if (subtotal - currentDiscount <= 0) {
            throw new AppError("La commande est déjà intégralement couverte par des remises", 400);
        }

        // 6. Persister dans une transaction + recalcul déterministe
        const result = await prisma.$transaction(async (tx) => {
            // Créer l'usage avec placeholder (sera recalculé)
            const usage = await tx.promoCodeUsage.create({
                data: {
                    promoCodeId: promo.id,
                    orderId,
                    shopId,
                    discountApplied: 0,
                },
            });

            // Incrémenter le compteur global
            await tx.promoCode.update({
                where: { id: promo.id },
                data: { currentUses: { increment: 1 } },
            });

            // Recalculer TOUTES les remises dans l'ordre déterministe
            const recalc = await this._recalculateAllDiscounts(tx, orderId, shopId);

            // Relire l'usage pour obtenir le discountApplied final
            const updatedUsage = await tx.promoCodeUsage.findUnique({ where: { id: usage.id } });

            return { usage: updatedUsage ?? usage, ...recalc };
        });

        // Vérifier que la remise a bien été > 0, sinon rollback implicite grâce à la vérif
        if (result.usage.discountApplied <= 0) {
            // Annuler : supprimer l'usage qui n'a rien apporté
            await prisma.$transaction(async (tx) => {
                await tx.promoCodeUsage.delete({ where: { id: result.usage.id } });
                await tx.promoCode.update({
                    where: { id: promo.id },
                    data: { currentUses: { decrement: 1 } },
                });
                await this._recalculateAllDiscounts(tx, orderId, shopId);
            });
            throw new AppError("La remise calculée est nulle (commande déjà couverte)", 400);
        }

        return {
            success: true,
            message: this._formatSuccessMessage(promo, result.usage.discountApplied),
            usage: {
                id: result.usage.id,
                promoCodeId: promo.id,
                promoCode: promo.code,
                promoLabel: promo.label,
                discountType: promo.discountType,
                discountApplied: result.usage.discountApplied,
                createdAt: result.usage.createdAt.toISOString(),
            },
            newTotal: result.newTotal,
            discountTotal: result.newDiscountTotal,
        };
    },

    async removeFromOrder(shopId: string, orderId: string, usageId: string) {
        const usage = await prisma.promoCodeUsage.findFirst({
            where: { id: usageId, orderId, shopId },
            include: { promoCode: true },
        });

        if (!usage) throw new AppError("Usage de code promo introuvable", 404);

        const order = await prisma.order.findFirst({
            where: { id: orderId, shopId },
        });

        if (!order) throw new AppError("Commande introuvable", 404);
        if (order.status === "PAID") throw new AppError("Impossible de modifier une commande payée", 400);

        const result = await prisma.$transaction(async (tx) => {
            // Supprimer l'usage
            await tx.promoCodeUsage.delete({ where: { id: usageId } });

            // Décrémenter le compteur global
            await tx.promoCode.update({
                where: { id: usage.promoCodeId },
                data: { currentUses: { decrement: 1 } },
            });

            // Recalculer TOUTES les remises dans l'ordre déterministe
            return this._recalculateAllDiscounts(tx, orderId, shopId);
        });

        return {
            success: true,
            message: `Code promo "${usage.promoCode.code}" retiré`,
            newTotal: result.newTotal,
            discountTotal: result.newDiscountTotal,
        };
    },

    // ── HELPERS PRIVÉS ───────────────────────────────────────────────

    _getEligibleItems(items: any[], promo: any) {
        if (promo.targetProductId) {
            return items.filter((i: any) => i.productId === promo.targetProductId);
        }
        if (promo.targetCategoryId) {
            return items.filter(
                (i: any) => i.product?.categoryId === promo.targetCategoryId,
            );
        }
        // Global — tous les items
        return items;
    },

    _formatSuccessMessage(promo: any, discount: number): string {
        const euros = (discount / 100).toFixed(2).replace(".", ",") + " €";
        switch (promo.discountType) {
            case "PERCENTAGE":
                return `Remise de ${(promo.discountValue / 100).toFixed(0)}% appliquée (−${euros})`;
            case "FIXED_AMOUNT":
                return `Remise de ${euros} appliquée`;
            case "FREE_PRODUCT":
                return `${promo.targetProduct?.name ?? "Produit"} offert(e) (−${euros})`;
            default:
                return `Remise de ${euros} appliquée`;
        }
    },

    /**
     * Recalcule TOUTES les remises d'une commande dans l'ordre déterministe :
     *   1. FREE_PRODUCT   — retire le prix unitaire du produit cible
     *   2. PERCENTAGE     — % sur le brut éligible, plafonné au restant
     *   3. FIXED_AMOUNT   — montant fixe, plafonné au restant
     *
     * Met à jour chaque promoCodeUsage.discountApplied + order.discountTotal/total.
     * Doit être appelé dans une transaction Prisma (tx).
     */
    async _recalculateAllDiscounts(
        tx: any,
        orderId: string,
        _shopId: string,
    ): Promise<{ newDiscountTotal: number; newTotal: number }> {
        // 1. Charger les usages avec leur promoCode + l'état courant de la commande
        const [usages, order] = await Promise.all([
            tx.promoCodeUsage.findMany({
                where: { orderId },
                include: {
                    promoCode: {
                        include: { targetProduct: true, targetCategory: true },
                    },
                },
            }),
            tx.order.findUnique({
                where: { id: orderId },
                select: { depositTotal: true },
            }),
        ])

        // 2. Charger les items non-annulés
        const orderItems = await tx.orderItem.findMany({
            where: { orderId, status: { not: "CANCELLED" } },
            include: { product: { include: { category: true } } },
        });

        const subtotal = orderItems.reduce(
            (sum: number, i: any) => sum + i.unitPrice * i.quantity, 0,
        );

        if (usages.length === 0) {
            const depositTotal = order?.depositTotal ?? 0
            // Pas de promos → reset
            await tx.order.update({
                where: { id: orderId },
                data: { discountTotal: 0, total: subtotal + depositTotal, revision: { increment: 1 } },
            });
            return { newDiscountTotal: 0, newTotal: subtotal + depositTotal };
        }

        // 3. Trier par priorité de type, puis par createdAt ASC (premier ajouté = premier servi)
        usages.sort((a: any, b: any) => {
            const pa = DISCOUNT_TYPE_PRIORITY[a.promoCode?.discountType] ?? 99;
            const pb = DISCOUNT_TYPE_PRIORITY[b.promoCode?.discountType] ?? 99;
            if (pa !== pb) return pa - pb;
            return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
        });

        // 4. Calculer chaque remise dans l'ordre
        let runningDiscount = 0;

        for (const usage of usages) {
            const promo = usage.promoCode;
            if (!promo) {
                // Promo orpheline, remise = 0
                await tx.promoCodeUsage.update({
                    where: { id: usage.id },
                    data: { discountApplied: 0 },
                });
                continue;
            }

            let discount = 0;
            const eligibleItems = this._getEligibleItems(orderItems, promo);

            switch (promo.discountType) {
                case "FREE_PRODUCT": {
                    const targetItem = eligibleItems.find(
                        (i: any) => i.productId === promo.targetProductId,
                    );
                    discount = targetItem ? targetItem.unitPrice : 0;
                    break;
                }
                case "PERCENTAGE": {
                    const eligibleTotal = eligibleItems.reduce(
                        (sum: number, i: any) => sum + i.unitPrice * i.quantity, 0,
                    );
                    discount = Math.round(eligibleTotal * promo.discountValue / 10000);
                    break;
                }
                case "FIXED_AMOUNT": {
                    discount = promo.discountValue;
                    break;
                }
            }

            // Plafonner au reste disponible
            const maxRemaining = subtotal - runningDiscount;
            discount = Math.max(Math.min(discount, maxRemaining), 0);

            // Persister
            await tx.promoCodeUsage.update({
                where: { id: usage.id },
                data: { discountApplied: discount },
            });

            runningDiscount += discount;
        }

        // 5. Mettre à jour la commande
        const newDiscountTotal = runningDiscount;
        const depositTotal = order?.depositTotal ?? 0
        const newTotal = Math.max(subtotal - newDiscountTotal, 0) + depositTotal;

        await tx.order.update({
            where: { id: orderId },
            data: {
                discountTotal: newDiscountTotal,
                total: newTotal,
                revision: { increment: 1 },
            },
        });

        return { newDiscountTotal, newTotal };
    },

    /**
     * Revalide les promos appliquées sur une commande après modification des items.
     * Retire les usages excédentaires si les items éligibles ont été retirés/réduits,
     * puis recalcule TOUTES les remises dans l'ordre déterministe.
     * Doit être appelé dans une transaction Prisma (tx).
     * Retourne le nouveau discountTotal.
     */
    async revalidateOrderPromos(tx: any, orderId: string, shopId: string): Promise<number> {
        const usages = await tx.promoCodeUsage.findMany({
            where: { orderId },
            include: {
                promoCode: {
                    include: { targetProduct: true, targetCategory: true },
                },
            },
            orderBy: { createdAt: "desc" }, // Plus récents d'abord (on retire les derniers ajoutés)
        });

        if (usages.length === 0) return 0;

        const orderItems = await tx.orderItem.findMany({
            where: { orderId, status: { not: "CANCELLED" } },
            include: { product: { include: { category: true } } },
        });

        // Grouper les usages par promoCodeId
        const usagesByPromo = new Map<string, any[]>();
        for (const u of usages) {
            const list = usagesByPromo.get(u.promoCodeId) ?? [];
            list.push(u);
            usagesByPromo.set(u.promoCodeId, list);
        }

        const usageIdsToRemove: string[] = [];

        for (const [promoCodeId, promoUsages] of usagesByPromo) {
            const promo = promoUsages[0].promoCode;
            if (!promo) continue;

            const eligibleItems = this._getEligibleItems(orderItems, promo);
            const eligibleQty = eligibleItems.reduce((sum: number, i: any) => sum + i.quantity, 0);

            // Combien d'usages sont autorisés maintenant ?
            const maxAllowed = Math.min(promo.maxUsesPerOrder, eligibleQty);
            const excess = promoUsages.length - maxAllowed;

            if (excess > 0) {
                // Retirer les usages excédentaires (les plus récents d'abord)
                const toRemove = promoUsages.slice(0, excess);
                for (const u of toRemove) {
                    usageIdsToRemove.push(u.id);
                }

                // Décrémenter le compteur global du promoCode
                await tx.promoCode.update({
                    where: { id: promoCodeId },
                    data: { currentUses: { decrement: excess } },
                });
            }
        }

        if (usageIdsToRemove.length > 0) {
            await tx.promoCodeUsage.deleteMany({
                where: { id: { in: usageIdsToRemove } },
            });
        }

        // Recalculer TOUTES les remises dans l'ordre déterministe
        const recalc = await this._recalculateAllDiscounts(tx, orderId, shopId);
        return recalc.newDiscountTotal;
    },
};

