import SumUp from "@sumup/sdk";
import { randomUUID } from "crypto";
import { AppError } from "@/core/AppError";
import { tracked } from "@/core/tracked";
import { prisma } from "@/core/prisma";
import { paymentRuntimeService } from "@/modules/payment/payment-runtime.service";
import { paymentService } from "@/modules/payment/payment.service";
import { paymentTerminalRealtimeService } from "@/modules/payment/payment-terminal-realtime.service";
import { sumupPaymentProviderAdapter, checkSumUpReaderAvailability } from "@/modules/payment/providers/sumup-payment-provider.adapter";
import { PaymentMethod, PaymentProvider, PaymentAttemptStatus } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import { startPolling, stopPolling } from "@/modules/payment/payment-attempt-poller";
import type { TapToPayActivateResponseDto, TapToPayEnvironment, TapToPayInitResponseDto } from "ttm-shared";

type SupportedSumUpMethod = "CARD" | "APPLE_PAY" | "GOOGLE_PAY"

const SUPPORTED_SUMUP_METHODS = new Set<SupportedSumUpMethod>([
    PaymentMethod.CARD,
    PaymentMethod.APPLE_PAY,
    PaymentMethod.GOOGLE_PAY
]);

function resolveSupportedSumUpMethod(method?: PaymentMethod): SupportedSumUpMethod {
    const resolved = method ?? PaymentMethod.CARD
    if (!SUPPORTED_SUMUP_METHODS.has(resolved as SupportedSumUpMethod)) {
        throw new AppError("Unsupported payment method for SumUp checkout", 400);
    }
    return resolved as SupportedSumUpMethod
}

function createSumUpClient(accessToken: string) {
    return new SumUp({ apiKey: accessToken });
}

type TapToPayStartOptions = {
    method?: PaymentMethod;
    stationId?: string | null;
    shopPaymentMethodConfigId?: string;
    stationPaymentMethodConfigId?: string;
    senderSocketId?: string | null;
}

function inspectSumUpWebhookEnvelope(payload: any) {
    const eventType = payload?.event_type ?? null

    let providerCheckoutId: string | null = null
    let webhookStatus: string | null = null
    let webhookTxId: string | null = null

    if (eventType === "solo.transaction.updated" || eventType === "solo.transaction.created") {
        providerCheckoutId = payload?.payload?.client_transaction_id ?? null
        webhookStatus = payload?.payload?.status?.toUpperCase() ?? null
        webhookTxId = payload?.payload?.transaction_id ?? null
    } else {
        providerCheckoutId = payload?.payload?.client_transaction_id ?? payload?.id ?? null
    }

    return {
        eventType,
        providerCheckoutId,
        webhookStatus,
        webhookTxId,
    }
}

async function requestTerminalRefreshForAttempt(
    attempt: {
        id: string
        shopId: string
        orderId: string
        stationId: string | null
        providerDeviceId: string | null
    },
    senderSocketId?: string | null,
    reason?: string | null,
) {
    await paymentTerminalRealtimeService.requestRefresh({
        shopId: attempt.shopId,
        deviceId: attempt.providerDeviceId,
        paymentAttemptId: attempt.id,
        orderId: attempt.orderId,
        stationId: attempt.stationId,
        senderSocketId: senderSocketId ?? null,
        reason: reason ?? undefined,
    })
}

async function resolveTapToPayPreparation(
    shopId: string,
    orderId: string,
    amount: number,
    options?: TapToPayStartOptions,
) {
    const method = resolveSupportedSumUpMethod(options?.method);

    const startInput = {
        amount,
        method,
        executionMethod: "TAP_TO_PAY" as const,
        ...(options?.stationId ? { stationId: options.stationId } : {}),
        ...(options?.shopPaymentMethodConfigId ? { shopPaymentMethodConfigId: options.shopPaymentMethodConfigId } : {}),
        ...(options?.stationPaymentMethodConfigId ? { stationPaymentMethodConfigId: options.stationPaymentMethodConfigId } : {})
    };

    const prepared = await paymentRuntimeService.preparePaymentAttempt(shopId, orderId, startInput);

    if (prepared.resolution.provider !== PaymentProvider.SUMUP) {
        throw new AppError("Resolved payment provider is not SUMUP", 400);
    }

    if (!prepared.resolution.accountPaymentProviderId) {
        throw new AppError("No SumUp provider account configured", 400);
    }

    const providerAccount = await prisma.accountPaymentProvider.findUnique({
        where: { id: prepared.resolution.accountPaymentProviderId }
    });

    if (!providerAccount || providerAccount.provider !== PaymentProvider.SUMUP || !providerAccount.active) {
        throw new AppError("Active SumUp provider account not found", 400);
    }

    const resolvedAccount = sumupPaymentProviderAdapter.resolveSumUpAccount({
        provider: providerAccount.provider,
        config: providerAccount.config,
        secrets: providerAccount.secrets
    });

    const configuredEnvironment = process.env.SUMUP_ENVIRONMENT?.trim().toLowerCase();
    const environment: TapToPayEnvironment | undefined = configuredEnvironment === "sandbox"
        ? "sandbox"
        : configuredEnvironment === "production"
            ? "production"
            : undefined;

    return {
        method,
        startInput,
        prepared,
        resolvedAccount,
        environment,
    };
}

type SumUpRuntimeError = Error & {
    code?: string;
    cause?: unknown;
    status?: number;
    response?: {
        status?: number;
        data?: unknown;
    };
}

const SUMUP_UNREACHABLE_CODES = new Set([
    "ECONNREFUSED",
    "ECONNRESET",
    "ETIMEDOUT",
    "ENOTFOUND",
    "EHOSTUNREACH",
    "UND_ERR_CONNECT_TIMEOUT",
    "UND_ERR_SOCKET",
])

function getErrorCodeList(error: unknown): string[] {
    if (!error || typeof error !== "object") return []

    const current = error as { code?: unknown; cause?: unknown }
    const nestedCause = current.cause && typeof current.cause === "object"
        ? current.cause as { code?: unknown }
        : null

    return [current.code, nestedCause?.code]
        .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
}

function getErrorMessageList(error: unknown): string[] {
    if (!error || typeof error !== "object") return []

    const current = error as { message?: unknown; cause?: unknown }
    const nestedCause = current.cause && typeof current.cause === "object"
        ? current.cause as { message?: unknown }
        : null

    return [current.message, nestedCause?.message]
        .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
}

function normalizeSumUpCheckoutError(error: unknown): AppError {
    if (error instanceof AppError) return error

    const codes = getErrorCodeList(error)
    const messages = getErrorMessageList(error).map((message) => message.toLowerCase())
    const joinedMessages = messages.join(" | ")

    if (
        codes.some((code) => SUMUP_UNREACHABLE_CODES.has(code))
        || joinedMessages.includes("fetch failed")
        || joinedMessages.includes("network")
        || joinedMessages.includes("socket")
        || joinedMessages.includes("timeout")
        || joinedMessages.includes("econnrefused")
        || joinedMessages.includes("reader offline")
        || joinedMessages.includes("device offline")
        || joinedMessages.includes("terminal offline")
    ) {
        return new AppError(
            "Le terminal SumUp est injoignable ou éteint. Vérifiez qu'il est allumé, connecté au réseau et prêt, puis réessayez.",
            503,
        )
    }

    return new AppError(
        (error as SumUpRuntimeError)?.message?.trim() || "Impossible de démarrer le paiement SumUp",
        502,
    )
}

function normalizeReaderAvailabilityReason(reason: string | null | undefined): string | null {
    const trimmed = reason?.trim().replace(/[\s.]+$/u, "")
    return trimmed ? trimmed : null
}

function buildReaderAvailabilityError(
    status: "ready" | "busy" | "offline" | "unknown",
    reason: string | null,
): AppError {
    const detail = normalizeReaderAvailabilityReason(reason)

    if (status === "busy") {
        if (!detail || /terminal\s+(sumup\s+)?occup[ée]/iu.test(detail) || /transaction en cours/iu.test(detail)) {
            return new AppError("Le terminal SumUp est déjà occupé par une autre opération.", 409)
        }

        return new AppError(`Le terminal SumUp est déjà occupé (${detail}).`, 409)
    }

    if (status === "offline") {
        if (!detail || /terminal\s+(sumup\s+)?hors ligne/iu.test(detail) || /terminal introuvable/iu.test(detail)) {
            return new AppError("Le terminal SumUp est hors ligne ou éteint.", 503)
        }

        return new AppError(`Le terminal SumUp est indisponible (${detail}).`, 503)
    }

    if (!detail || /impossible de (joindre|vérifier) le statut du terminal/iu.test(detail)) {
        return new AppError("Impossible de joindre le terminal SumUp pour le moment.", 503)
    }

    return new AppError(`Impossible de joindre le terminal SumUp pour le moment (${detail}).`, 503)
}


export const sumupService = {
    async inspectWebhookPayload(payload: any) {
        const envelope = inspectSumUpWebhookEnvelope(payload)

        if (!envelope.providerCheckoutId) {
            return {
                dryRun: true,
                ok: false,
                message: "Payload webhook invalide : aucun providerCheckoutId exploitable trouvé",
                eventType: envelope.eventType,
                providerCheckoutId: null,
                webhookStatus: envelope.webhookStatus,
                webhookTransactionId: envelope.webhookTxId,
                matchedAttemptId: null,
                matchedOrderId: null,
            }
        }

        const attempt = await prisma.paymentAttempt.findUnique({
            where: { providerCheckoutId: envelope.providerCheckoutId },
            select: {
                id: true,
                orderId: true,
                status: true,
            }
        })

        return {
            dryRun: true,
            ok: true,
            message: attempt
                ? `Payload SumUp parsé correctement. Une tentative locale serait ciblée (${attempt.id}).`
                : "Payload SumUp parsé correctement, mais aucune tentative locale ne correspond à ce checkout.",
            eventType: envelope.eventType,
            providerCheckoutId: envelope.providerCheckoutId,
            webhookStatus: envelope.webhookStatus,
            webhookTransactionId: envelope.webhookTxId,
            matchedAttemptId: attempt?.id ?? null,
            matchedOrderId: attempt?.orderId ?? null,
        }
    },

    async createCheckout(
        shopId: string,
        orderId: string,
        amount: number,
        options?: {
            method?: PaymentMethod;
            executionMethod?: "READER";
            stationId?: string | null;
            shopPaymentMethodConfigId?: string;
            stationPaymentMethodConfigId?: string;
            senderSocketId?: string | null;
        }
    ) {
        const method = resolveSupportedSumUpMethod(options?.method);

        console.log(`[SumUp] createCheckout START orderId=${orderId} amount=${amount} method=${method}`)

        const existing = await prisma.paymentAttempt.findFirst({
            where: {
                orderId,
                shopId,
                provider: PaymentProvider.SUMUP,
                status: PaymentAttemptStatus.PENDING,
                method,
                amount,
                providerCheckoutId: { not: null }
            }
        });

        if (existing) {
            console.log(`[SumUp] Reusing existing attempt ${existing.id} checkoutId=${existing.providerCheckoutId}`)
            const snapshot = (existing.configSnapshot as Record<string, any> | null) ?? {};
            const sumupSnapshot = (snapshot.sumup as Record<string, any> | undefined) ?? {};

            // Relancer le polling pour cet attempt existant aussi
            startPolling(existing.id);

            return {
                attemptId: existing.id,
                paymentAttemptId: existing.id,
                checkoutId: existing.providerCheckoutId,
                flowType: typeof sumupSnapshot.flowType === "string" ? sumupSnapshot.flowType : "reader"
            };
        }

        console.log(`[SumUp] No existing attempt found, creating new one...`)

        await prisma.paymentAttempt.updateMany({
            where: {
                orderId,
                shopId,
                provider: PaymentProvider.SUMUP,
                status: PaymentAttemptStatus.PENDING,
                providerCheckoutId: null
            },
            data: {
                status: PaymentAttemptStatus.FAILED,
                failureReason: "Superseded by a newer SumUp checkout"
            }
        });

        const startInput = {
            amount,
            method,
            ...(options?.executionMethod ? { executionMethod: options.executionMethod } : {}),
            ...(options?.stationId ? { stationId: options.stationId } : {}),
            ...(options?.shopPaymentMethodConfigId ? { shopPaymentMethodConfigId: options.shopPaymentMethodConfigId } : {}),
            ...(options?.stationPaymentMethodConfigId ? { stationPaymentMethodConfigId: options.stationPaymentMethodConfigId } : {})
        };

        const preparedAttempt = await paymentRuntimeService.preparePaymentAttempt(
            shopId,
            orderId,
            startInput,
        );

        const resolution = preparedAttempt.resolution;
        if (resolution.provider !== PaymentProvider.SUMUP) {
            throw new AppError("Resolved payment provider is not SUMUP", 400);
        }

        if (!resolution.accountPaymentProviderId) {
            throw new AppError("No SumUp provider account configured", 400);
        }

        const providerAccount = await prisma.accountPaymentProvider.findUnique({
            where: { id: resolution.accountPaymentProviderId }
        });

        if (!providerAccount || providerAccount.provider !== PaymentProvider.SUMUP || !providerAccount.active) {
            throw new AppError("Active SumUp provider account not found", 400);
        }

        const resolvedAccount = sumupPaymentProviderAdapter.resolveSumUpAccount({
            provider: providerAccount.provider,
            config: providerAccount.config,
            secrets: providerAccount.secrets
        });

        const providerDevice = resolution.providerDeviceId
            ? await prisma.providerDevice.findUnique({ where: { id: resolution.providerDeviceId } })
            : null;

        if (providerDevice && !providerDevice.active) {
            throw new AppError("Configured SumUp device is inactive", 400);
        }

        console.log(`[SumUp] flowType=reader deviceId=${providerDevice?.id ?? "none"} externalId=${providerDevice?.externalId ?? "none"}`)

        if (providerDevice?.externalId) {
            const readerAvailability = await checkSumUpReaderAvailability(
                resolvedAccount.config.merchantCode,
                providerDevice.externalId,
                resolvedAccount.secrets.apiSecretKey,
            )

            if (!readerAvailability.available) {
                throw buildReaderAvailabilityError(readerAvailability.status, readerAvailability.reason)
            }
        }

        const client = createSumUpClient(resolvedAccount.secrets.apiSecretKey);

        // URL du webhook : priorité à la config du provider account, fallback sur env
        const webhookBaseUrl = resolvedAccount.config.webhookUrl
            ?? process.env.PUBLIC_WEBHOOK_URL
            ?? null;

        let webhookUrl: string | undefined;
        if (webhookBaseUrl) {
            // Si l'admin a déjà mis le path complet (contient /sumup/webhook ou
            // /api/sumup/webhook), on l'utilise tel quel.
            if (webhookBaseUrl.includes("/sumup/webhook")) {
                webhookUrl = webhookBaseUrl;
            } else {
                // Sinon on ajoute le path canonique (toutes les routes dynamiques
                // vivent sous /api). Le vhost Apache conserve un alias legacy
                // /sumup/webhook → /api/sumup/webhook pour ne pas casser les
                // webhooks déjà enregistrés côté SumUp.
                const base = webhookBaseUrl.replace(/\/+$/, ""); // retire le trailing slash
                webhookUrl = `${base}/api/sumup/webhook`;
            }
        }

        console.log(`[SumUp] Webhook URL: ${webhookUrl ?? "NOT CONFIGURED"}`)

        if (!webhookUrl) {
            console.warn(`[SumUp] ⚠️  No webhook URL configured (neither provider config nor PUBLIC_WEBHOOK_URL env)`)
        }

        const started = await paymentRuntimeService.createPreparedPaymentAttempt(
            shopId,
            orderId,
            startInput,
            preparedAttempt,
            options?.senderSocketId ?? null
        );

        console.log(`[SumUp] PaymentAttempt created: ${started.attempt.id} provider=${started.resolution.provider}`)

        try {
            let checkoutId: string;

            // Seul le flow reader est supporté pour le moment
            const flowType = "reader" as const;

            if (!providerDevice?.externalId) {
                throw new AppError("Aucun terminal de paiement (reader) configuré pour ce compte SumUp", 400);
            }

            // ── READER FLOW ──────────────────────────────────────
            console.log(`[SumUp] Reader flow: sending to reader ${providerDevice.externalId}`)
            const readerResponse: any = await client.readers.createCheckout(
                    resolvedAccount.config.merchantCode,
                    providerDevice!.externalId,
                    {
                        description: "Payment via SumUp reader",
                        ...(webhookUrl ? { return_url: webhookUrl } : {}),
                        total_amount: {
                            currency: "EUR",
                            minor_unit: 2,
                            value: amount,
                        },
                    }
                );

                const readerData = readerResponse?.data ?? readerResponse;
                checkoutId = readerData?.client_transaction_id
                    ?? readerData?.id
                    ?? readerResponse?.client_transaction_id;

                console.log(`[SumUp] Reader response:`, JSON.stringify(readerResponse).substring(0, 500))
                console.log(`[SumUp] Reader checkout client_transaction_id=${checkoutId}`)


            if (!checkoutId) {
                console.error(`[SumUp] ❌ checkoutId is falsy after creation! Value: ${JSON.stringify(checkoutId)}`)
                throw new AppError("SumUp did not return a checkout ID", 502)
            }

            const currentSnapshot = (started.attempt.configSnapshot ?? {}) as Record<string, unknown>;
            const mergedSnapshot = {
                ...currentSnapshot,
                sumup: {
                    flowType,
                    merchantCode: resolvedAccount.config.merchantCode,
                    readerExternalId: providerDevice?.externalId ?? null,
                    checkoutId,
                }
            } satisfies Record<string, unknown>;

            console.log(`[SumUp] Writing checkoutId=${checkoutId} to PaymentAttempt ${started.attempt.id}...`)

            const updatedAttempt = await prisma.paymentAttempt.update({
                where: { id: started.attempt.id },
                data: {
                    providerCheckoutId: checkoutId,
                    configSnapshot: mergedSnapshot as Prisma.InputJsonValue
                }
            });

            console.log(`[SumUp] ✅ DB updated: attemptId=${updatedAttempt.id} providerCheckoutId=${updatedAttempt.providerCheckoutId}`)

            // Démarrer le polling backend (reader: poll transaction history)
            startPolling(started.attempt.id);

            return {
                attemptId: started.attempt.id,
                paymentAttemptId: started.attempt.id,
                checkoutId,
                flowType,
                resolution
            };
        } catch (err) {
            const normalizedError = normalizeSumUpCheckoutError(err)
            console.error(`[SumUp] ❌ Checkout creation FAILED for attempt ${started.attempt.id}:`, {
                raw: err instanceof Error ? err.message : err,
                normalized: normalizedError.message,
                codes: getErrorCodeList(err),
            })
            await prisma.paymentAttempt.updateMany({
                where: {
                    id: started.attempt.id,
                    status: PaymentAttemptStatus.PENDING,
                    payment: null
                },
                data: {
                    status: PaymentAttemptStatus.FAILED,
                    failureReason: normalizedError.message
                }
            });

            const t = tracked({ shopId, entity: "PaymentAttempt", senderSocketId: options?.senderSocketId ?? null });
            await t.emit(
                "payment.attempt_failed",
                {
                    paymentAttemptId: started.attempt.id,
                    orderId,
                    amount,
                    method,
                    provider: PaymentProvider.SUMUP,
                    stationId: started.attempt.stationId,
                    reason: normalizedError.message
                },
                started.attempt.id,
                1
            );

            await requestTerminalRefreshForAttempt(started.attempt, options?.senderSocketId ?? null, normalizedError.message)

            throw normalizedError;
        }
    },

    async processWebhook(payload: any) {
        console.log(`[SumUp Webhook] Received:`, JSON.stringify(payload).substring(0, 500))

        const {
            eventType,
            providerCheckoutId,
            webhookStatus,
            webhookTxId,
        } = inspectSumUpWebhookEnvelope(payload)

        if (eventType === "solo.transaction.updated" || eventType === "solo.transaction.created") {
            console.log(`[SumUp Webhook] Reader event: client_transaction_id=${providerCheckoutId} status=${webhookStatus} tx_id=${webhookTxId}`)
        } else {
            console.log(`[SumUp Webhook] Unknown event_type=${eventType}, trying to extract id...`)
        }

        if (!providerCheckoutId) {
            console.warn("[SumUp Webhook] Could not extract checkout/transaction ID, ignoring.")
            return null;
        }

        // Chercher l'attempt par providerCheckoutId
        const attempt = await prisma.paymentAttempt.findUnique({
            where: { providerCheckoutId },
            include: {
                payment: true,
                accountPaymentProvider: true
            }
        });

        if (!attempt) {
            console.warn(`[SumUp Webhook] No attempt found for providerCheckoutId=${providerCheckoutId}, ignoring.`);
            return null;
        }

        console.log(`[SumUp Webhook] Matched attempt=${attempt.id} status=${attempt.status}`)

        // Si le webhook porte déjà le statut final (reader flow), on peut l'utiliser directement
        // sans re-interroger l'API SumUp
        if (webhookStatus === "SUCCESSFUL" || webhookStatus === "PAID") {
            stopPolling(attempt.id);

            if (attempt.payment) {
                return {
                    orderId: attempt.orderId,
                    status: "PAID",
                    paymentId: attempt.payment.id
                };
            }

            return paymentService.addPayment(
                attempt.shopId,
                attempt.orderId,
                attempt.amount,
                attempt.method,
                webhookTxId ?? undefined,
                attempt.id,
                null
            );
        }

        if (webhookStatus === "FAILED" || webhookStatus === "CANCELLED" || webhookStatus === "DECLINED") {
            stopPolling(attempt.id);

            const updated = await prisma.paymentAttempt.updateMany({
                where: {
                    id: attempt.id,
                    payment: null,
                    status: PaymentAttemptStatus.PENDING
                },
                data: {
                    status: PaymentAttemptStatus.FAILED,
                    failureReason: webhookStatus
                },
            });

            if (updated.count > 0) {
                const t = tracked({ shopId: attempt.shopId, entity: "PaymentAttempt" });
                await t.emit(
                    "payment.attempt_failed",
                    {
                        paymentAttemptId: attempt.id,
                        orderId: attempt.orderId,
                        amount: attempt.amount,
                        method: attempt.method,
                        provider: attempt.provider,
                        stationId: attempt.stationId,
                        reason: webhookStatus
                    },
                    attempt.id,
                    1
                );

                await requestTerminalRefreshForAttempt(attempt, null, webhookStatus)
            }

            return { orderId: attempt.orderId, status: "FAILED" };
        }

        // Si le statut n'est pas dans le webhook, interroger l'API pour vérifier
        if (!attempt.accountPaymentProvider) {
            throw new AppError("SumUp provider account missing for webhook processing", 500);
        }

        const checkResult = await sumupPaymentProviderAdapter.checkPaymentStatus!(
            providerCheckoutId,
            attempt.accountPaymentProvider,
            attempt.configSnapshot as Record<string, any> | null
        );

        console.log(`[SumUp Webhook] checkResult status=${checkResult.status} attemptId=${attempt.id}`);

        if (checkResult.status === "PAID") {
            stopPolling(attempt.id);

            if (attempt.payment) {
                return {
                    orderId: attempt.orderId,
                    status: "PAID",
                    paymentId: attempt.payment.id
                };
            }

            return paymentService.addPayment(
                attempt.shopId,
                attempt.orderId,
                attempt.amount,
                attempt.method,
                checkResult.providerTxId ?? undefined,
                attempt.id,
                null
            );
        }

        if (checkResult.status === "FAILED") {
            stopPolling(attempt.id);
            const updated = await prisma.paymentAttempt.updateMany({
                where: {
                    id: attempt.id,
                    payment: null,
                    status: PaymentAttemptStatus.PENDING
                },
                data: {
                    status: PaymentAttemptStatus.FAILED,
                    failureReason: checkResult.failureReason ?? "SumUp checkout failed"
                },
            });

            if (updated.count > 0) {
                const t = tracked({ shopId: attempt.shopId, entity: "PaymentAttempt" });
                await t.emit(
                    "payment.attempt_failed",
                    {
                        paymentAttemptId: attempt.id,
                        orderId: attempt.orderId,
                        amount: attempt.amount,
                        method: attempt.method,
                        provider: attempt.provider,
                        stationId: attempt.stationId,
                        reason: checkResult.failureReason ?? "SumUp checkout failed"
                    },
                    attempt.id,
                    1
                );

                await requestTerminalRefreshForAttempt(attempt, null, checkResult.failureReason ?? "SumUp checkout failed")
            }

            return { orderId: attempt.orderId, status: "FAILED" };
        }

        console.log(`[SumUp Webhook] Status still PENDING for attempt=${attempt.id}, ignoring.`)
        return null;
    },

    /**
     * TAP_TO_PAY — Étape 1 : prépare le contexte sans créer de PaymentAttempt.
     * On ne crée l'attempt qu'une fois que l'app Android confirme l'ouverture du flow natif.
     */
    async initiateTapToPay(
        shopId: string,
        orderId: string,
        amount: number,
        options?: TapToPayStartOptions
    ): Promise<TapToPayInitResponseDto> {
        const { method, prepared, resolvedAccount, environment } = await resolveTapToPayPreparation(
            shopId,
            orderId,
            amount,
            options,
        );

        console.log(`[SumUp TapToPay] prepare orderId=${orderId} amount=${amount} method=${method}`)

        return {
            requestId: randomUUID(),
            flowType: "tap_to_pay" as const,
            orderId,
            amount,
            currency: "EUR",
            resolution: prepared.resolution,
            sumupAccessToken: resolvedAccount.secrets.apiSecretKey,
            accessToken: resolvedAccount.secrets.apiSecretKey,
            environment,
            merchantId: resolvedAccount.config.merchantCode,
        };
    },

    /**
     * TAP_TO_PAY — Étape 2 : l'app Android confirme l'ouverture du flow natif,
     * on peut alors créer la PaymentAttempt côté backend.
     */
    async activateTapToPay(
        shopId: string,
        requestId: string,
        orderId: string,
        amount: number,
        options?: TapToPayStartOptions,
    ): Promise<TapToPayActivateResponseDto> {
        const { method, prepared, startInput } = await resolveTapToPayPreparation(
            shopId,
            orderId,
            amount,
            options,
        );

        console.log(`[SumUp TapToPay] activate requestId=${requestId} orderId=${orderId} amount=${amount} method=${method}`)

        await prisma.paymentAttempt.updateMany({
            where: {
                orderId,
                shopId,
                provider: PaymentProvider.SUMUP,
                executionMethod: "TAP_TO_PAY",
                status: PaymentAttemptStatus.PENDING,
                providerCheckoutId: null,
                payment: null,
            },
            data: {
                status: PaymentAttemptStatus.FAILED,
                failureReason: "Superseded by a newer Tap To Pay initialization"
            }
        });

        const started = await paymentRuntimeService.createPreparedPaymentAttempt(
            shopId,
            orderId,
            startInput,
            prepared,
            options?.senderSocketId ?? null,
        );

        console.log(`[SumUp TapToPay] PaymentAttempt created after native confirmation: ${started.attempt.id}`)

        return {
            requestId,
            attempt: started.attempt,
            paymentAttemptId: started.attempt.id,
            flowType: "tap_to_pay",
            orderId,
            amount,
            currency: "EUR",
            resolution: started.resolution,
        };
    },

    /**
     * TAP_TO_PAY — Étape 2 : L'app Flutter signale le résultat du SDK SumUp.
     * Si succès → finalise le paiement. Si échec → marque l'attempt FAILED.
     */
    async completeTapToPay(
        shopId: string,
        attemptId: string,
        result: {
            success: boolean;
            transactionCode?: string | null;
            failureReason?: string | null;
        },
        senderSocketId?: string | null
    ) {
        console.log(`[SumUp TapToPay] complete attemptId=${attemptId} success=${result.success}`)

        const attempt = await prisma.paymentAttempt.findFirst({
            where: { id: attemptId, shopId, status: PaymentAttemptStatus.PENDING }
        });

        if (!attempt) {
            throw new AppError("Payment attempt not found or already processed", 404);
        }

        if (result.success) {
            // Mettre à jour le providerTxId si fourni
            if (result.transactionCode) {
                await prisma.paymentAttempt.update({
                    where: { id: attemptId },
                    data: { providerTxId: result.transactionCode }
                });
            }

            // Finaliser le paiement
            const paymentResult = await paymentService.addPayment(
                attempt.shopId,
                attempt.orderId,
                attempt.amount,
                attempt.method,
                result.transactionCode ?? undefined,
                attempt.id,
                senderSocketId
            );

            console.log(`[SumUp TapToPay] ✅ Payment finalized for attempt ${attemptId}`)
            return {
                status: "PAID" as const,
                paymentId: paymentResult.paymentId,
                orderId: attempt.orderId,
            };
        } else {
            // Marquer l'attempt comme échoué
            await prisma.paymentAttempt.updateMany({
                where: { id: attemptId, status: PaymentAttemptStatus.PENDING, payment: null },
                data: {
                    status: PaymentAttemptStatus.FAILED,
                    failureReason: result.failureReason ?? "Tap To Pay failed"
                }
            });

            const t = tracked({ shopId: attempt.shopId, entity: "PaymentAttempt", senderSocketId });
            await t.emit(
                "payment.attempt_failed",
                {
                    paymentAttemptId: attempt.id,
                    orderId: attempt.orderId,
                    amount: attempt.amount,
                    method: attempt.method,
                    provider: PaymentProvider.SUMUP,
                    stationId: attempt.stationId,
                    reason: result.failureReason ?? "Tap To Pay failed"
                },
                attempt.id,
                1
            );

            await requestTerminalRefreshForAttempt(attempt, senderSocketId, result.failureReason ?? "Tap To Pay failed")

            console.log(`[SumUp TapToPay] ❌ Attempt ${attemptId} marked FAILED: ${result.failureReason}`)
            return {
                status: "FAILED" as const,
                orderId: attempt.orderId,
                reason: result.failureReason ?? "Tap To Pay failed",
            };
        }
    },

    /**
     * Vérifie la disponibilité d'un reader SumUp.
     * Combine la vérification API SumUp + la vérification locale (attempt PENDING sur ce device).
     */
    async checkReaderStatus(shopId: string, deviceId: string) {
        console.log(`[SumUp] checkReaderStatus deviceId=${deviceId}`)

        // 1. Récupérer le device en base avec son provider account
        const device = await prisma.providerDevice.findUnique({
            where: { id: deviceId },
            include: { accountPaymentProvider: true }
        });

        if (!device || !device.active) {
            return { deviceId, available: false, status: "offline" as const, reason: "Terminal non trouvé ou inactif" };
        }

        if (device.accountPaymentProvider.provider !== PaymentProvider.SUMUP) {
            return { deviceId, available: false, status: "unknown" as const, reason: "Ce terminal n'est pas un SumUp" };
        }

        // 2. Vérifier s'il y a un PaymentAttempt PENDING local sur ce device
        const pendingAttempt = await prisma.paymentAttempt.findFirst({
            where: {
                shopId,
                providerDeviceId: deviceId,
                status: PaymentAttemptStatus.PENDING
            },
            select: { id: true, orderId: true, createdAt: true }
        });

        if (pendingAttempt) {
            console.log(`[SumUp] Reader ${deviceId} has pending attempt ${pendingAttempt.id}`)
            return {
                deviceId,
                available: false,
                status: "busy" as const,
                reason: "Transaction en cours sur ce terminal"
            };
        }

        // 3. Interroger l'API SumUp pour le statut réel du reader
        const resolved = sumupPaymentProviderAdapter.resolveSumUpAccount({
            provider: device.accountPaymentProvider.provider,
            config: device.accountPaymentProvider.config,
            secrets: device.accountPaymentProvider.secrets
        });

        const result = await checkSumUpReaderAvailability(
            resolved.config.merchantCode,
            device.externalId,
            resolved.secrets.apiSecretKey
        );

        return { deviceId, ...result };
    },

    /**
     * Annule un PaymentAttempt PENDING en envoyant l'ordre d'annulation au provider
     * (ex: abort reader checkout sur le TPE SumUp Solo).
     */
    async cancelCheckout(
        shopId: string,
        attemptId: string,
        senderSocketId?: string | null
    ) {
        console.log(`[SumUp] cancelCheckout attemptId=${attemptId}`)

        const attempt = await prisma.paymentAttempt.findFirst({
            where: { id: attemptId, shopId, status: PaymentAttemptStatus.PENDING },
            include: {
                accountPaymentProvider: true,
                providerDevice: { include: { accountPaymentProvider: true } }
            }
        });

        if (!attempt) {
            throw new AppError("Tentative de paiement introuvable ou déjà traitée", 404);
        }

        console.log(`[SumUp] cancelCheckout: providerCheckoutId=${attempt.providerCheckoutId} accountProvider=${!!attempt.accountPaymentProvider} providerDevice=${attempt.providerDevice?.externalId ?? "none"} configSnapshot keys=${Object.keys((attempt.configSnapshot as any) ?? {})}`)

        // Stopper le polling backend immédiatement
        stopPolling(attemptId);

        // Envoyer l'annulation au provider si possible
        let cancelReason = "Paiement annulé par le caissier";

        // Stratégie 1 : via l'adapter classique (utilise configSnapshot)
        if (attempt.providerCheckoutId && attempt.accountPaymentProvider) {
            try {
                const cancelResult = await sumupPaymentProviderAdapter.cancelAttempt!(
                    attempt.providerCheckoutId,
                    attempt.accountPaymentProvider,
                    attempt.configSnapshot as Record<string, any> | null
                );
                console.log(`[SumUp] cancelAttempt via adapter result:`, cancelResult);
            } catch (err) {
                console.warn(`[SumUp] cancelAttempt adapter call failed:`, err);
            }
        }
        // Stratégie 2 : fallback direct si on a le device mais pas de checkoutId
        // (annulation rapide avant que le checkout soit enregistré)
        else if (attempt.providerDevice?.externalId) {
            console.log(`[SumUp] cancelCheckout: fallback via direct reader terminate (no checkoutId yet)`)
            const account = attempt.providerDevice.accountPaymentProvider ?? attempt.accountPaymentProvider;
            if (account) {
                try {
                    const resolved = sumupPaymentProviderAdapter.resolveSumUpAccount({
                        provider: account.provider,
                        config: account.config,
                        secrets: account.secrets
                    });
                    const response = await fetch(
                        `https://api.sumup.com/v0.1/merchants/${encodeURIComponent(resolved.config.merchantCode)}/readers/${encodeURIComponent(attempt.providerDevice.externalId)}/terminate`,
                        {
                            method: "POST",
                            headers: {
                                Authorization: `Bearer ${resolved.secrets.apiSecretKey}`,
                                "Content-Type": "application/json",
                            },
                            body: JSON.stringify({}),
                        }
                    );
                    console.log(`[SumUp] cancelCheckout: direct terminate response status=${response.status}`);
                } catch (err) {
                    console.warn(`[SumUp] cancelCheckout: direct terminate failed:`, err);
                }
            }
        } else {
            console.warn(`[SumUp] cancelCheckout: cannot send cancel to provider — no checkoutId (${attempt.providerCheckoutId}) and no device (${attempt.providerDeviceId})`);
        }

        // Marquer l'attempt comme CANCELLED en base
        await prisma.paymentAttempt.updateMany({
            where: {
                id: attemptId,
                status: PaymentAttemptStatus.PENDING,
                payment: null
            },
            data: {
                status: PaymentAttemptStatus.CANCELLED,
                failureReason: cancelReason
            }
        });

        // Émettre un event WS pour notifier les autres clients
        const t = tracked({ shopId: attempt.shopId, entity: "PaymentAttempt", senderSocketId });
        await t.emit(
            "payment.attempt_failed",
            {
                paymentAttemptId: attempt.id,
                orderId: attempt.orderId,
                amount: attempt.amount,
                method: attempt.method,
                provider: PaymentProvider.SUMUP,
                stationId: attempt.stationId,
                reason: cancelReason
            },
            attempt.id,
            1
        );

        await requestTerminalRefreshForAttempt(attempt, senderSocketId, cancelReason)

        console.log(`[SumUp] ✅ Checkout cancelled for attempt ${attemptId}`)
        return {
            status: "CANCELLED" as const,
            orderId: attempt.orderId,
            attemptId: attempt.id
        };
    }
};
