import { AppError } from "../../core/AppError";
import { OrderStatus, PaymentMethod } from "@prisma/client";
import { tracked, trackedTransaction } from "@/core/tracked";
import { CARD_GROUP_METHODS } from "ttm-shared";
import { paymentTerminalRealtimeService } from "./payment-terminal-realtime.service";

const FULL_AMOUNT_METHODS = new Set<PaymentMethod>(CARD_GROUP_METHODS as PaymentMethod[]);

async function resolveCashDrawerImpactMode(tx: any, shopId: string, method: PaymentMethod) {
    const config = await tx.shopPaymentMethodConfig.findFirst({
        where: { shopId, method, enabled: true },
        select: { cashDrawerImpactMode: true }
    });

    return (config?.cashDrawerImpactMode ?? "NONE") as "NONE" | "INCREASE" | "DECREASE";
}

function resolveOrderPaymentStatus(orderTotal: number, totalPaid: number): OrderStatus {
    if (totalPaid <= 0) {
        return OrderStatus.PENDING_PAYMENT;
    }

    if (totalPaid >= orderTotal) {
        return OrderStatus.PAID;
    }

    return OrderStatus.PARTIALLY_PAID;
}

function isOrderFullyDelivered(orderItems: Array<{ status: string | null }>) {
    const activeItems = orderItems.filter((item) => item.status !== "CANCELLED")
    if (activeItems.length === 0) return true

    return activeItems.every((item) => !item.status || item.status === "DELIVERED")
}

function collectProductionStationIds(
    order: {
        orderItems?: Array<{
            currentStepStationId?: string | null
            product?: { stationId?: string | null } | null
        }>
    } | null | undefined,
) {
    return [...new Set(
        (order?.orderItems ?? [])
            .map((item) => item.currentStepStationId ?? item.product?.stationId ?? null)
            .filter((value): value is string => typeof value === "string")
    )]
}

function withOrderRealtimeScope<T extends Record<string, any>>(
    payload: T,
    order: {
        stationId?: string | null
        cashSessionId?: string | null
        orderItems?: Array<{
            currentStepStationId?: string | null
            product?: { stationId?: string | null } | null
        }>
    } | null | undefined,
) {
    return {
        ...payload,
        stationId: order?.stationId ?? null,
        cashSessionId: order?.cashSessionId ?? null,
        productionStationIds: collectProductionStationIds(order),
    }
}

function serializeDetailedOrder(order: any) {
    const attempts = order.paymentAttempt ?? []
    const lastAttempt = attempts.length > 0
        ? attempts.sort((left: any, right: any) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime())[0]
        : null

    return {
        ...order,
        depositTotal: order.depositTotal ?? 0,
        discountTotal: order.discountTotal ?? 0,
        cashSessionId: order.cashSessionId ?? null,
        depositLines: (order.depositLines ?? []).map((line: any) => ({
            id: line.id,
            depositDefinitionId: line.depositDefinitionId,
            label: line.labelSnapshot,
            unitAmount: line.unitAmountSnapshot,
            quantity: line.quantity,
            totalAmount: line.totalAmount,
        })),
        orderItems: (order.orderItems ?? []).map((item: any) => ({
            id: item.id,
            name: item.name,
            productId: item.productId,
            quantity: item.quantity,
            unitPrice: item.unitPrice,
            status: item.status,
            orderId: item.orderId,
            revision: item.revision,
            stationId: item.product?.stationId ?? null,
            stationName: item.product?.station?.name ?? null,
            createdAt: item.createdAt,
            takenByName: item.takenBy?.displayName ?? item.takenBy?.email ?? null,
            pickedUpByName: (() => {
                const lastPickedUp = (item.lifecycleEvents ?? []).filter((entry: any) => entry.eventType === "PICKED_UP").slice(-1)[0]
                return lastPickedUp?.actorName
                    ?? item.pickedUpBy?.displayName
                    ?? item.pickedUpBy?.username
                    ?? null
            })(),
            recipeId: item.recipeId ?? null,
            recipeName: item.recipeName ?? null,
            logisticsRouteId: item.logisticsRouteId ?? null,
            currentStepPosition: item.currentStepPosition ?? null,
            currentStepStationId: item.currentStepStationId ?? null,
            transportedByName: item.transportedByName ?? null,
            customizations: (item.customizations ?? []).map((customization: any) => ({
                id: customization.id,
                ingredientId: customization.ingredientId,
                ingredientName: customization.ingredientName,
                action: customization.action,
                quantity: customization.quantity,
                extraPrice: customization.extraPrice,
            })),
            lifecycleEvents: (item.lifecycleEvents ?? []).map((event: any) => ({
                id: event.id,
                eventType: event.eventType,
                fromStatus: event.fromStatus,
                toStatus: event.toStatus,
                actorId: event.actorId,
                actorName: event.actorName,
                stationId: event.stationId,
                occurredAt: typeof event.occurredAt === "string" ? event.occurredAt : event.occurredAt.toISOString(),
            })),
        })),
        payments: (order.payments ?? []).map((payment: any) => ({
            id: payment.id,
            amount: payment.amount,
            method: payment.method,
            executionMethod: payment.executionMethod ?? null,
            provider: payment.provider ?? null,
            reference: payment.reference ?? null,
            createdAt: typeof payment.createdAt === "string" ? payment.createdAt : payment.createdAt.toISOString(),
        })),
        appliedPromoCodes: (order.promoCodeUsages ?? []).map((usage: any) => ({
            id: usage.id,
            promoCodeId: usage.promoCodeId,
            promoCode: usage.promoCode?.code ?? "",
            promoLabel: usage.promoCode?.label ?? null,
            discountType: usage.promoCode?.discountType ?? "FIXED_AMOUNT",
            discountApplied: usage.discountApplied,
            createdAt: usage.createdAt,
        })),
        lastPaymentAttempt: lastAttempt ? {
            id: lastAttempt.id,
            status: lastAttempt.status,
            method: lastAttempt.method,
            amount: lastAttempt.amount,
            failureReason: lastAttempt.failureReason ?? null,
            createdAt: lastAttempt.createdAt,
        } : null,
        paymentAttempt: undefined,
        promoCodeUsages: undefined,
    }
}

export const paymentService = {
    async addPayment(
        shopId: string,
        orderId: string,
        amount: number,
        method: PaymentMethod,
        reference?: string,
        paymentAttemptId?: string,
        senderSocketId?: string | null
    ) {
        const result = await trackedTransaction(shopId, "Order", async (tOrder, tx: any) => {
            const order = await tx.order.findFirst({
                where: { id: orderId, shopId },
                include: {
                    payments: true,
                    orderItems: {
                        select: {
                            currentStepStationId: true,
                            product: {
                                select: {
                                    stationId: true,
                                }
                            }
                        }
                    }
                }
            });

            if (!order) {
                throw new AppError("Order not found", 404);
            }

            const totalPaid = order.payments.reduce(
                (sum: number, p: { amount: number }) => sum + p.amount,
                0
            );

            const remaining = order.total - totalPaid;
            if (remaining <= 0) {
                throw new AppError("Order is already fully paid", 400);
            }

            if (FULL_AMOUNT_METHODS.has(method) && amount !== remaining) {
                throw new AppError("Invalid card payment amount", 400);
            }

            if (method !== PaymentMethod.CASH && amount > remaining) {
                throw new AppError("Payment amount exceeds the remaining balance", 400);
            }

            const appliedAmount = method === PaymentMethod.CASH
                ? Math.min(amount, remaining)
                : amount;

            let paymentAttempt = paymentAttemptId
                ? await tx.paymentAttempt.findFirst({
                    where: { id: paymentAttemptId, orderId, shopId },
                    include: { payment: true }
                })
                : null;

            if (paymentAttemptId && !paymentAttempt) {
                throw new AppError("Payment attempt not found", 404);
            }

            if (paymentAttempt) {
                if (paymentAttempt.method !== method) {
                    throw new AppError("Payment attempt method mismatch", 400);
                }

                if (paymentAttempt.amount !== appliedAmount) {
                    throw new AppError("Payment attempt amount mismatch", 400);
                }

                if (paymentAttempt.status === "FAILED" || paymentAttempt.status === "CANCELLED") {
                    throw new AppError("Payment attempt cannot be finalized", 400);
                }

                if (paymentAttempt.payment) {
                    throw new AppError("Payment attempt already linked to a payment", 400);
                }

                paymentAttempt = await tx.paymentAttempt.update({
                    where: { id: paymentAttempt.id },
                    data: {
                        status: "PAID",
                        ...(reference ? { providerTxId: reference } : {})
                    }
                });
            } else {
                const cashDrawerImpactMode = await resolveCashDrawerImpactMode(tx, shopId, method)
                paymentAttempt = await tx.paymentAttempt.create({
                    data: {
                        orderId,
                        shopId,
                        stationId: order.stationId ?? null,
                        amount: appliedAmount,
                        currency: "EUR",
                        method,
                        executionMethod: "MANUAL",
                        status: "PAID",
                        provider: null,
                        configSnapshot: {
                            source: "manual",
                            method,
                            executionMethod: "MANUAL",
                            stationId: order.stationId ?? null,
                            cashDrawerImpactMode,
                        }
                    }
                });
            }

            const payment = await tx.payment.create({
                data: {
                    orderId,
                    shopId,
                    stationId: paymentAttempt.stationId ?? order.stationId ?? null,
                    paymentAttemptId: paymentAttempt.id,
                    amount: appliedAmount,
                    method: paymentAttempt.method,
                    executionMethod: paymentAttempt.executionMethod,
                    provider: paymentAttempt.provider,
                    accountPaymentProviderId: paymentAttempt.accountPaymentProviderId,
                    providerDeviceId: paymentAttempt.providerDeviceId,
                    shopPaymentMethodConfigId: paymentAttempt.shopPaymentMethodConfigId,
                    stationPaymentMethodConfigId: paymentAttempt.stationPaymentMethodConfigId,
                    reference,
                    configSnapshot: paymentAttempt.configSnapshot ?? {
                        source: "manual",
                        method: paymentAttempt.method
                    }
                }
            });

            const newTotalPaid = totalPaid + appliedAmount;
            const newStatus =
                newTotalPaid >= order.total
                    ? OrderStatus.PAID
                    : OrderStatus.PARTIALLY_PAID;

            await tOrder.update(
                "order",
                { id: orderId },
                {
                    status: newStatus,
                    paidAt: newStatus === OrderStatus.PAID ? new Date() : null
                },
                undefined,
                "order.status_changed",
                withOrderRealtimeScope({
                    orderId,
                    previousStatus: order.status,
                    newStatus
                }, order)
            );

            return {
                status: newStatus,
                change: method === PaymentMethod.CASH && amount > remaining
                    ? amount - remaining
                    : 0,
                appliedAmount,
                paymentId: payment.id,
                paymentAttemptId: paymentAttempt.id,
                paymentAttemptMethod: paymentAttempt.method,
                paymentAttemptProvider: paymentAttempt.provider,
                paymentAttemptStationId: paymentAttempt.stationId ?? order.stationId ?? null,
                paymentAttemptProviderDeviceId: paymentAttempt.providerDeviceId ?? null,
            };
        }, { senderSocketId });

        const tPaymentAttempt = tracked({
            shopId,
            entity: "PaymentAttempt",
            senderSocketId
        });

        await tPaymentAttempt.emit(
            "payment.attempt_succeeded",
            {
                paymentAttemptId: result.paymentAttemptId,
                orderId,
                paymentId: result.paymentId,
                amount: result.appliedAmount,
                method: result.paymentAttemptMethod,
                provider: result.paymentAttemptProvider,
                stationId: result.paymentAttemptStationId
            },
            result.paymentAttemptId,
            1
        );

        await paymentTerminalRealtimeService.requestRefresh({
            shopId,
            deviceId: result.paymentAttemptProviderDeviceId,
            paymentAttemptId: result.paymentAttemptId,
            orderId,
            stationId: result.paymentAttemptStationId,
            senderSocketId,
            reason: "Paiement finalisé, vérification immédiate du terminal demandée",
        })

        return {
            status: result.status,
            change: result.change,
            appliedAmount: result.appliedAmount,
            paymentId: result.paymentId,
            paymentAttemptId: result.paymentAttemptId
        };
    },

    async cancelPayment(
        shopId: string,
        orderId: string,
        paymentId: string,
        actorUserId?: string,
        senderSocketId?: string | null,
    ) {
        return trackedTransaction(shopId, "Order", async (tOrder, tx: any) => {
            const order = await tx.order.findFirst({
                where: { id: orderId, shopId },
                include: {
                    payments: true,
                    orderItems: {
                        select: {
                            id: true,
                            status: true,
                            currentStepStationId: true,
                            product: {
                                select: {
                                    stationId: true,
                                }
                            }
                        }
                    }
                }
            })

            if (!order) {
                throw new AppError("Order not found", 404)
            }

            if (order.status === OrderStatus.CANCELLED) {
                throw new AppError("Cannot cancel a payment for a cancelled order", 400)
            }

            if (isOrderFullyDelivered(order.orderItems ?? [])) {
                throw new AppError("Cannot cancel a payment for a finalized order", 400)
            }

            const payment = order.payments.find((entry: any) => entry.id === paymentId)
            if (!payment) {
                throw new AppError("Payment not found", 404)
            }

            const latestPayment = [...(order.payments ?? [])]
                .sort((left: any, right: any) => {
                    const dateDiff = new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime()
                    if (dateDiff !== 0) return dateDiff
                    return right.id.localeCompare(left.id)
                })[0]

            if (!latestPayment || latestPayment.id !== payment.id) {
                throw new AppError("Only the latest payment can be cancelled from the cashier", 400)
            }

            if (payment.amount <= 0) {
                throw new AppError("This payment cannot be cancelled", 400)
            }

            if (payment.executionMethod !== "MANUAL") {
                throw new AppError("Only manual payments can be cancelled from the cashier", 400)
            }

            if (payment.paymentAttemptId) {
                await tx.paymentAttempt.update({
                    where: { id: payment.paymentAttemptId },
                    data: {
                        status: "CANCELLED",
                        failureReason: "Cancelled by cashier before order finalization"
                    }
                })
            }

            await tx.payment.delete({
                where: { id: payment.id }
            })

            const remainingPayments = order.payments.filter((entry: any) => entry.id !== payment.id)
            const totalPaid = remainingPayments.reduce((sum: number, entry: { amount: number }) => sum + entry.amount, 0)
            const newStatus = resolveOrderPaymentStatus(order.total, totalPaid)
            const nextPaidAt = newStatus === OrderStatus.PAID ? order.paidAt ?? new Date() : null

            if (newStatus !== order.status || (order.paidAt ?? null) !== nextPaidAt) {
                await tOrder.update(
                    "order",
                    { id: orderId },
                    {
                        status: newStatus,
                        paidAt: nextPaidAt,
                    },
                    undefined,
                    "order.status_changed",
                    withOrderRealtimeScope({
                        orderId,
                        previousStatus: order.status,
                        newStatus,
                        paidAt: nextPaidAt ? nextPaidAt.toISOString() : null,
                        paymentCancelled: true,
                        cancelledPaymentId: payment.id,
                        cancelledPaymentAmount: payment.amount,
                        cancelledPaymentMethod: payment.method,
                    }, order)
                )
            } else {
                await tOrder.emit(
                    "order.updated",
                    {
                        orderId,
                        paymentsChanged: true,
                        paymentCancelled: true,
                        cancelledPaymentId: payment.id,
                        cancelledPaymentAmount: payment.amount,
                        cancelledPaymentMethod: payment.method,
                    },
                    orderId,
                    order.revision,
                )
            }

            const refreshedOrder = await tx.order.findUnique({
                where: { id: orderId },
                include: {
                    depositLines: true,
                    orderItems: {
                        include: {
                            product: { include: { station: true } },
                            takenBy: true,
                            pickedUpBy: true,
                            customizations: true,
                            lifecycleEvents: {
                                orderBy: { occurredAt: "asc" as const }
                            },
                        }
                    },
                    payments: true,
                    paymentAttempt: {
                        orderBy: { createdAt: "desc" as const },
                        take: 5
                    },
                    promoCodeUsages: {
                        include: {
                            promoCode: { select: { code: true, label: true, discountType: true } }
                        }
                    }
                }
            })

            return serializeDetailedOrder(refreshedOrder)
        }, { actorUserId, senderSocketId })
    }
};