import { AppError } from "@/core/AppError"
import { tracked } from "@/core/tracked"
import { prisma } from "@/core/prisma"
import type { Prisma, PaymentMethod as PrismaPaymentMethod, ExecutionMethod as PrismaExecutionMethod } from "@prisma/client"
import { PaymentAttemptStatus } from "@prisma/client"
import type {
    PaymentAttemptDto,
    PaymentExecutionOptionDto,
    PaymentMethod as SharedPaymentMethod,
    ExecutionMethod as SharedExecutionMethod,
    ResolvedPaymentConfigDto,
    StartPaymentAttemptInput,
    StartPaymentAttemptResultDto,
    PaymentOptionDto,
    ManualResolvePaymentAttemptInputDto as ManualResolvePaymentAttemptInput
} from "ttm-shared"
import {
    PAYMENT_PROVIDER_DEFINITIONS,
    CARD_GROUP_METHODS,
    EXECUTION_METHOD_LABELS,
    PAYMENT_METHOD_LABELS
} from "ttm-shared"
import { paymentService } from "./payment.service"
import { paymentTerminalRealtimeService } from "./payment-terminal-realtime.service"

/** Méthodes de paiement qui ne nécessitent aucun provider (executionMethod = MANUAL) */
const MANUAL_METHODS = new Set<PrismaPaymentMethod>(["CASH", "CHECK", "GIFT_CARD", "VOUCHER"])

/** Méthodes qui requièrent le montant exact restant (pas de rendu de monnaie possible) */
const FULL_AMOUNT_METHODS = new Set<PrismaPaymentMethod>(CARD_GROUP_METHODS as PrismaPaymentMethod[])

/** Set pour vérifier rapidement si une méthode fait partie du groupe carte */
const CARD_GROUP_SET = new Set<string>(CARD_GROUP_METHODS)

function toSharedPaymentMethod(method: PrismaPaymentMethod): SharedPaymentMethod {
    return method as SharedPaymentMethod
}

function toSharedExecutionMethod(method: PrismaExecutionMethod): SharedExecutionMethod {
    return method as SharedExecutionMethod
}

function readCustomPaymentLabel(metadata: unknown): string | null {
    if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) return null
    const candidate = (metadata as Record<string, unknown>).customLabel
    return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : null
}

function resolvePaymentMethodLabel(method: SharedPaymentMethod, metadata?: unknown): string {
    const customLabel = readCustomPaymentLabel(metadata)
    return customLabel ?? PAYMENT_METHOD_LABELS[method] ?? method
}

type ShopPaymentConfigRecord = Prisma.ShopPaymentMethodConfigGetPayload<{
    include: {
        accountPaymentProvider: true
        defaultProviderDevice: {
            include: {
                accountPaymentProvider: true
            }
        }
    }
}>

type StationPaymentConfigRecord = Prisma.StationPaymentMethodConfigGetPayload<{
    include: {
        providerDevice: true
        shopPaymentMethodConfig: true
    }
}>

type EffectivePaymentConfigCandidate = {
    shopConfig: ShopPaymentConfigRecord
    stationConfig: StationPaymentConfigRecord | null
    effectiveDevice: ShopPaymentConfigRecord["defaultProviderDevice"] | StationPaymentConfigRecord["providerDevice"] | null
    availableExecutionMethods: SharedExecutionMethod[]
    source: ResolvedPaymentConfigDto["source"]
}

type ManualResolvePaymentAttemptResult = {
    attemptId: string
    orderId: string
    status: "PAID" | "FAILED"
    paymentId: string | null
    failureReason: string | null
}

type PreparedPaymentAttempt = {
    remaining: number
    resolution: ResolvedPaymentConfigDto
}

function isCardGroupMethod(method: PrismaPaymentMethod | SharedPaymentMethod): boolean {
    return CARD_GROUP_SET.has(method)
}

function toExposedMethod(method: PrismaPaymentMethod): SharedPaymentMethod {
    const sharedMethod = toSharedPaymentMethod(method)
    return isCardGroupMethod(sharedMethod) ? "CARD" : sharedMethod
}

function matchesConfiguredMethod(requestedMethod: PrismaPaymentMethod, configuredMethod: PrismaPaymentMethod): boolean {
    if (requestedMethod === configuredMethod) return true
    return isCardGroupMethod(requestedMethod) && isCardGroupMethod(configuredMethod)
}

function buildExecutionLabel(
    executionMethod: SharedExecutionMethod,
    providerLabel: string | null,
    providerDeviceLabel: string | null
): string {
    const baseLabel = EXECUTION_METHOD_LABELS[executionMethod] ?? executionMethod
    const detailParts = executionMethod === "READER"
        ? [providerLabel, providerDeviceLabel].filter((value): value is string => !!value?.trim())
        : [providerLabel].filter((value): value is string => !!value?.trim())

    return detailParts.length > 0
        ? `${baseLabel} — ${detailParts.join(" · ")}`
        : baseLabel
}

function getAvailableExecutionMethods(
    config: ShopPaymentConfigRecord,
    effectiveDevice: EffectivePaymentConfigCandidate["effectiveDevice"]
): SharedExecutionMethod[] {
    if (MANUAL_METHODS.has(config.method)) {
        return ["MANUAL"]
    }

    const provider = config.accountPaymentProvider?.provider
    if (!provider) return []

    const providerDef = PAYMENT_PROVIDER_DEFINITIONS[provider as keyof typeof PAYMENT_PROVIDER_DEFINITIONS]
    const supportedEntry = providerDef?.supportedMethods?.find((entry) => entry.method === toExposedMethod(config.method))
    if (!supportedEntry) return []

    return supportedEntry.executionMethods.filter((executionMethod) => {
        if (executionMethod === "READER") {
            return !!effectiveDevice?.externalId
        }

        return true
    })
}

function toExecutionOption(
    candidate: EffectivePaymentConfigCandidate,
    executionMethod: SharedExecutionMethod
): PaymentExecutionOptionDto {
    const providerLabel = candidate.shopConfig.accountPaymentProvider?.label ?? null
    const providerDeviceLabel = candidate.effectiveDevice?.label ?? null

    return {
        executionMethod,
        label: buildExecutionLabel(executionMethod, providerLabel, providerDeviceLabel),
        provider: candidate.shopConfig.accountPaymentProvider?.provider ?? null,
        providerLabel,
        providerDeviceId: executionMethod === "TAP_TO_PAY" ? null : candidate.effectiveDevice?.id ?? null,
        providerDeviceLabel: executionMethod === "TAP_TO_PAY" ? null : providerDeviceLabel,
        shopPaymentMethodConfigId: candidate.shopConfig.id,
        stationPaymentMethodConfigId: candidate.stationConfig?.id ?? null,
        source: candidate.source
    }
}

function resolveRequestedExecutionMethod(
    availableExecutionMethods: SharedExecutionMethod[],
    requestedExecutionMethod?: string | null
): SharedExecutionMethod {
    if (requestedExecutionMethod) {
        if (!availableExecutionMethods.includes(requestedExecutionMethod as SharedExecutionMethod)) {
            throw new AppError("The selected execution method is not available for this provider configuration", 400)
        }

        return requestedExecutionMethod as SharedExecutionMethod
    }

    if (availableExecutionMethods.length === 1) {
        return availableExecutionMethods[0]!
    }

    throw new AppError("Multiple execution methods are available for this provider configuration", 409)
}

async function listCandidatePaymentConfigs(
    shopId: string,
    method: PrismaPaymentMethod,
    stationId?: string | null,
    requireExplicitStationConfig = false,
    requestedShopConfigId?: string,
    requestedStationConfigId?: string
): Promise<EffectivePaymentConfigCandidate[]> {
    const [shopConfigs, stationConfigs] = await Promise.all([
        prisma.shopPaymentMethodConfig.findMany({
            where: {
                shopId,
                enabled: true,
                ...(requestedShopConfigId ? { id: requestedShopConfigId } : {})
            },
            include: {
                accountPaymentProvider: true,
                defaultProviderDevice: { include: { accountPaymentProvider: true } }
            },
            orderBy: [{ displayOrder: "asc" }, { createdAt: "asc" }, { id: "asc" }]
        }),
        stationId
            ? prisma.stationPaymentMethodConfig.findMany({
                where: {
                    stationId,
                    ...(requestedStationConfigId ? { id: requestedStationConfigId } : {})
                },
                include: {
                    providerDevice: true,
                    shopPaymentMethodConfig: true
                }
            })
            : Promise.resolve([] as StationPaymentConfigRecord[])
    ])

    const stationConfigByShopConfigId = new Map<string, StationPaymentConfigRecord>()
    for (const stationConfig of stationConfigs) {
        stationConfigByShopConfigId.set(stationConfig.shopPaymentMethodConfigId, stationConfig)
    }

    return shopConfigs
        .filter((shopConfig) => matchesConfiguredMethod(method, shopConfig.method))
        .map<EffectivePaymentConfigCandidate | null>((shopConfig) => {
            const stationConfig = stationConfigByShopConfigId.get(shopConfig.id) ?? null
            if (requireExplicitStationConfig && !stationConfig) return null
            if (stationConfig && !stationConfig.enabled) return null

            const effectiveDevice = stationConfig?.providerDevice ?? shopConfig.defaultProviderDevice ?? null
            const availableExecutionMethods = getAvailableExecutionMethods(shopConfig, effectiveDevice)
            if (availableExecutionMethods.length === 0) return null

            return {
                shopConfig,
                stationConfig,
                effectiveDevice,
                availableExecutionMethods,
                source: stationConfig ? "station_override" : "shop_default"
            }
        })
        .filter((candidate): candidate is EffectivePaymentConfigCandidate => candidate !== null)
}

function toPaymentAttemptDto(row: {
    id: string
    orderId: string
    shopId: string
    stationId: string | null
    amount: number
    currency: string
    method: PrismaPaymentMethod
    executionMethod: PrismaExecutionMethod | null
    provider: ResolvedPaymentConfigDto["provider"]
    accountPaymentProviderId: string | null
    providerDeviceId: string | null
    shopPaymentMethodConfigId: string | null
    stationPaymentMethodConfigId: string | null
    providerCheckoutId: string | null
    providerTxId: string | null
    status: PaymentAttemptDto["status"]
    failureReason: string | null
    configSnapshot: Prisma.JsonValue | null
    createdAt: Date
    updatedAt: Date
}): PaymentAttemptDto {
    return {
        id: row.id,
        orderId: row.orderId,
        shopId: row.shopId,
        stationId: row.stationId,
        amount: row.amount,
        currency: row.currency,
        method: toSharedPaymentMethod(row.method),
        executionMethod: row.executionMethod ? toSharedExecutionMethod(row.executionMethod) : null,
        provider: row.provider,
        accountPaymentProviderId: row.accountPaymentProviderId,
        providerDeviceId: row.providerDeviceId,
        shopPaymentMethodConfigId: row.shopPaymentMethodConfigId,
        stationPaymentMethodConfigId: row.stationPaymentMethodConfigId,
        providerCheckoutId: row.providerCheckoutId,
        providerTxId: row.providerTxId,
        status: row.status,
        failureReason: row.failureReason,
        configSnapshot: (row.configSnapshot as Record<string, unknown> | null) ?? null,
        createdAt: row.createdAt.toISOString(),
        updatedAt: row.updatedAt.toISOString()
    }
}

function ensurePaymentAmount(method: PrismaPaymentMethod, amount: number, remaining: number) {
    if (remaining <= 0) {
        throw new AppError("Order is already fully paid", 400)
    }
    if (FULL_AMOUNT_METHODS.has(method) && amount !== remaining) {
        throw new AppError("This payment method requires the exact remaining amount", 400)
    }
    if (method !== "CASH" && amount > remaining) {
        throw new AppError("Payment amount exceeds the remaining balance", 400)
    }
}

export const paymentRuntimeService = {

    async listOrderPaymentAttempts(shopId: string, orderId: string): Promise<PaymentAttemptDto[]> {
        const order = await prisma.order.findFirst({
            where: { id: orderId, shopId },
            select: { id: true }
        })

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

        const attempts = await prisma.paymentAttempt.findMany({
            where: { shopId, orderId },
            orderBy: [{ createdAt: "desc" }, { id: "desc" }]
        })

        return attempts.map((attempt) => toPaymentAttemptDto(attempt))
    },

    async getPaymentOptions(shopId: string, stationId?: string | null): Promise<PaymentOptionDto[]> {
        const station = stationId
            ? await prisma.station.findFirst({
                where: { id: stationId, shopId },
                select: { id: true, mainType: true }
            })
            : null
        const requireExplicitStationConfig = station?.mainType === "CASH"

        const shopConfigs = await prisma.shopPaymentMethodConfig.findMany({
            where: { shopId, enabled: true },
            include: {
                accountPaymentProvider: true,
                defaultProviderDevice: { include: { accountPaymentProvider: true } }
            },
            orderBy: [{ displayOrder: "asc" }, { createdAt: "asc" }, { id: "asc" }]
        })

        const stationConfigs = stationId
            ? await prisma.stationPaymentMethodConfig.findMany({
                where: { stationId },
                include: {
                    providerDevice: true,
                    shopPaymentMethodConfig: true
                }
            })
            : []

        const optionsByMethod = new Map<SharedPaymentMethod, PaymentOptionDto>()
        const stationConfigByShopConfigId = new Map<string, StationPaymentConfigRecord>()
        for (const stationConfig of stationConfigs) {
            stationConfigByShopConfigId.set(stationConfig.shopPaymentMethodConfigId, stationConfig)
        }

        for (const shopConfig of shopConfigs) {
            const stationConfig = stationConfigByShopConfigId.get(shopConfig.id) ?? null
            if (requireExplicitStationConfig && !stationConfig) continue
            if (stationConfig && !stationConfig.enabled) continue

            const effectiveDevice = stationConfig?.providerDevice ?? shopConfig.defaultProviderDevice ?? null
            const availableExecutionMethods = getAvailableExecutionMethods(shopConfig, effectiveDevice)
            if (availableExecutionMethods.length === 0) continue

            const method = toExposedMethod(shopConfig.method)
            const existing = optionsByMethod.get(method)
            const option = existing ?? {
                method,
                label: resolvePaymentMethodLabel(method, shopConfig.metadata),
                executionMethods: []
            }

            const candidate: EffectivePaymentConfigCandidate = {
                shopConfig,
                stationConfig,
                effectiveDevice,
                availableExecutionMethods,
                source: stationConfig ? "station_override" : "shop_default"
            }

            option.executionMethods.push(
                ...availableExecutionMethods.map((executionMethod) => toExecutionOption(candidate, executionMethod))
            )

            if (!existing) {
                optionsByMethod.set(method, option)
            }
        }

        const hasConfiguredCashMethod = shopConfigs.some((config) => config.method === "CASH")
            || stationConfigs.some((config) => config.shopPaymentMethodConfig.method === "CASH")

        if (!requireExplicitStationConfig && !optionsByMethod.has("CASH") && !hasConfiguredCashMethod) {
            optionsByMethod.set("CASH", {
                method: "CASH",
                label: PAYMENT_METHOD_LABELS.CASH,
                executionMethods: [{
                    executionMethod: "MANUAL",
                    label: EXECUTION_METHOD_LABELS.MANUAL,
                    provider: null,
                    providerLabel: null,
                    providerDeviceId: null,
                    providerDeviceLabel: null,
                    shopPaymentMethodConfigId: null,
                    stationPaymentMethodConfigId: null,
                    source: "shop_default"
                }]
            })
        }

        const options = [...optionsByMethod.values()]
        const cashIndex = options.findIndex((option) => option.method === "CASH")
        if (cashIndex > 0) {
            const [cashOption] = options.splice(cashIndex, 1)
            if (cashOption) options.unshift(cashOption)
        }


        return options
    },

    async resolvePaymentConfig(
        shopId: string,
        orderId: string,
        method: PrismaPaymentMethod,
        requestedStationId?: string | null,
        requestedExecutionMethod?: string | null,
        requestedShopPaymentMethodConfigId?: string,
        requestedStationPaymentMethodConfigId?: string
    ): Promise<{ order: { id: string; shopId: string; total: number; stationId: string | null; payments: { amount: number }[] }; remaining: number; resolution: ResolvedPaymentConfigDto }> {
        const order = await prisma.order.findFirst({
            where: { id: orderId, shopId },
            include: { payments: { select: { amount: true } } }
        })
        if (!order) throw new AppError("Order not found", 404)

        const totalPaid = order.payments.reduce((sum, p) => sum + p.amount, 0)
        const remaining = Math.max(order.total - totalPaid, 0)

        const stationId = requestedStationId ?? order.stationId ?? null
        const station = stationId
            ? await prisma.station.findFirst({
                where: { id: stationId, shopId, active: true },
                select: { id: true, mainType: true }
            })
            : null
        if (stationId && !station) throw new AppError("Station not found for this shop", 400)
        const requireExplicitStationConfig = station?.mainType === "CASH"

        const candidates = await listCandidatePaymentConfigs(
            shopId,
            method,
            stationId,
            requireExplicitStationConfig,
            requestedShopPaymentMethodConfigId,
            requestedStationPaymentMethodConfigId
        )

        const hasConfiguredCashMethod = method === "CASH"
            ? await prisma.shopPaymentMethodConfig.count({ where: { shopId, method: "CASH" } }) > 0
            : true

        if (candidates.length === 0) {
            if (method === "CASH" && !requireExplicitStationConfig && !hasConfiguredCashMethod && !requestedShopPaymentMethodConfigId && !requestedStationPaymentMethodConfigId) {
                const fallbackResolution: ResolvedPaymentConfigDto = {
                    source: "shop_default",
                    shopId,
                    orderId: order.id,
                    stationId,
                    method: "CASH",
                    executionMethod: "MANUAL",
                    provider: null,
                    accountPaymentProviderId: null,
                    accountPaymentProviderLabel: null,
                    providerDeviceId: null,
                    providerDeviceLabel: null,
                    cashDrawerImpactMode: "NONE",
                    shopPaymentMethodConfigId: "",
                    stationPaymentMethodConfigId: null,
                    configSnapshot: {
                        source: "shop_default",
                        shopPaymentMethodConfigId: null,
                        stationPaymentMethodConfigId: null,
                        method: "CASH",
                        executionMethod: "MANUAL",
                        provider: null,
                        accountPaymentProviderId: null,
                        accountPaymentProviderLabel: null,
                        providerDeviceId: null,
                        providerDeviceLabel: null,
                        cashDrawerImpactMode: "NONE",
                        stationId
                    }
                }

                return {
                    order: { id: order.id, shopId: order.shopId, total: order.total, stationId: order.stationId ?? null, payments: order.payments },
                    remaining,
                    resolution: fallbackResolution
                }
            }

            throw new AppError("Payment method is not configured for this shop or station", 400)
        }

        if (!requestedShopPaymentMethodConfigId && !requestedStationPaymentMethodConfigId && candidates.length > 1) {
            throw new AppError("Multiple payment provider configurations are available for this method. Please choose one explicitly.", 409)
        }

        const candidate = candidates[0]!
        const shopConfig = candidate.shopConfig
        const stationConfig = candidate.stationConfig
        const providerAccount = shopConfig.accountPaymentProvider
        const resolvedProviderDevice = candidate.effectiveDevice

        if (providerAccount && !providerAccount.active) throw new AppError("Selected provider account is inactive", 400)
        if (resolvedProviderDevice && !resolvedProviderDevice.active) throw new AppError("Selected provider device is inactive", 400)
        if (resolvedProviderDevice && providerAccount && resolvedProviderDevice.accountPaymentProviderId !== providerAccount.id) {
            throw new AppError("Provider device does not belong to the configured provider account", 400)
        }
        if (!MANUAL_METHODS.has(method) && !providerAccount) throw new AppError("This payment method requires an active provider account", 400)

        const executionMethod = resolveRequestedExecutionMethod(
            candidate.availableExecutionMethods,
            requestedExecutionMethod
        )

        // Pour TAP_TO_PAY, le "terminal" est le téléphone Android du vendeur,
        // pas un reader physique — on ne doit pas associer de ProviderDevice.
        const effectiveDevice = executionMethod === "TAP_TO_PAY" ? null : resolvedProviderDevice

        const source: ResolvedPaymentConfigDto["source"] = stationConfig ? "station_override" : "shop_default"
        const resolution: ResolvedPaymentConfigDto = {
            source, shopId, orderId: order.id, stationId,
            method: toSharedPaymentMethod(method),
            executionMethod: toSharedExecutionMethod(executionMethod),
            provider: providerAccount?.provider ?? null,
            accountPaymentProviderId: providerAccount?.id ?? null,
            accountPaymentProviderLabel: providerAccount?.label ?? null,
            providerDeviceId: effectiveDevice?.id ?? null,
            providerDeviceLabel: effectiveDevice?.label ?? null,
            cashDrawerImpactMode: (shopConfig as any).cashDrawerImpactMode ?? "NONE",
            shopPaymentMethodConfigId: shopConfig.id,
            stationPaymentMethodConfigId: stationConfig?.id ?? null,
            configSnapshot: {
                source,
                shopPaymentMethodConfigId: shopConfig.id,
                shopConfigRevision: shopConfig.revision,
                stationPaymentMethodConfigId: stationConfig?.id ?? null,
                method: toSharedPaymentMethod(method),
                executionMethod,
                provider: providerAccount?.provider ?? null,
                accountPaymentProviderId: providerAccount?.id ?? null,
                accountPaymentProviderLabel: providerAccount?.label ?? null,
                providerDeviceId: effectiveDevice?.id ?? null,
                providerDeviceLabel: effectiveDevice?.label ?? null,
                cashDrawerImpactMode: (shopConfig as any).cashDrawerImpactMode ?? "NONE",
                stationId
            }
        }

        return {
            order: { id: order.id, shopId: order.shopId, total: order.total, stationId: order.stationId ?? null, payments: order.payments },
            remaining, resolution
        }
    },

    async startPaymentAttempt(
        shopId: string,
        orderId: string,
        input: StartPaymentAttemptInput,
        senderSocketId?: string | null
    ): Promise<StartPaymentAttemptResultDto> {
        const prepared = await this.preparePaymentAttempt(shopId, orderId, input)
        return this.createPreparedPaymentAttempt(shopId, orderId, input, prepared, senderSocketId)
    },

    async preparePaymentAttempt(
        shopId: string,
        orderId: string,
        input: StartPaymentAttemptInput,
    ): Promise<PreparedPaymentAttempt> {
        const { remaining, resolution } = await this.resolvePaymentConfig(
            shopId, orderId, input.method as PrismaPaymentMethod,
            input.stationId ?? null,
            input.executionMethod ?? null,
            input.shopPaymentMethodConfigId,
            input.stationPaymentMethodConfigId
        )
        ensurePaymentAmount(input.method as PrismaPaymentMethod, input.amount, remaining)

        return { remaining, resolution }
    },

    async createPreparedPaymentAttempt(
        shopId: string,
        orderId: string,
        input: StartPaymentAttemptInput,
        prepared: PreparedPaymentAttempt,
        senderSocketId?: string | null
    ): Promise<StartPaymentAttemptResultDto> {
        const { remaining, resolution } = prepared

        const attempt = await prisma.paymentAttempt.create({
            data: {
                orderId, shopId,
                stationId: resolution.stationId,
                method: input.method as PrismaPaymentMethod,
                executionMethod: resolution.executionMethod as PrismaExecutionMethod,
                provider: resolution.provider ?? undefined,
                accountPaymentProviderId: resolution.accountPaymentProviderId,
                providerDeviceId: resolution.providerDeviceId,
                shopPaymentMethodConfigId: resolution.shopPaymentMethodConfigId,
                stationPaymentMethodConfigId: resolution.stationPaymentMethodConfigId,
                amount: input.amount,
                currency: "EUR",
                status: "PENDING",
                configSnapshot: resolution.configSnapshot as Prisma.InputJsonValue
            }
        })

        const t = tracked({ shopId, entity: "PaymentAttempt", senderSocketId })
        await t.emit(
            "payment.attempt_started",
            {
                paymentAttemptId: attempt.id, orderId,
                amount: attempt.amount, method: attempt.method,
                executionMethod: attempt.executionMethod,
                provider: attempt.provider, stationId: attempt.stationId,
                source: resolution.source
            },
            attempt.id, 1
        )

        if (resolution.executionMethod === "READER" && resolution.providerDeviceId) {
            await paymentTerminalRealtimeService.markBusy({
                shopId,
                deviceId: resolution.providerDeviceId,
                paymentAttemptId: attempt.id,
                orderId,
                stationId: attempt.stationId,
                senderSocketId,
            })
        }

        return { resolution, attempt: toPaymentAttemptDto(attempt), remaining }
    },

    async manualResolvePaymentAttempt(
        shopId: string,
        attemptId: string,
        input: ManualResolvePaymentAttemptInput,
        senderSocketId?: string | null
    ): Promise<ManualResolvePaymentAttemptResult> {
        const attempt = await prisma.paymentAttempt.findUnique({
            where: { id: attemptId },
            include: { payment: true }
        })

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

        if (attempt.shopId !== shopId) {
            throw new AppError("Forbidden", 403)
        }

        if (attempt.status !== PaymentAttemptStatus.PENDING) {
            throw new AppError(`Payment attempt is already ${attempt.status}`, 409)
        }

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

        if (input.status === "PAID") {
            const result = await paymentService.addPayment(
                shopId,
                attempt.orderId,
                attempt.amount,
                attempt.method,
                input.reference,
                attempt.id,
                senderSocketId
            )

            return {
                attemptId: attempt.id,
                orderId: attempt.orderId,
                status: "PAID",
                paymentId: result.paymentId,
                failureReason: null
            }
        }

        const reason = input.reason ?? "Marked as failed manually from POS"
        const updated = await prisma.paymentAttempt.updateMany({
            where: { id: attempt.id, status: PaymentAttemptStatus.PENDING, payment: null },
            data: {
                status: PaymentAttemptStatus.FAILED,
                failureReason: reason
            }
        })

        if (updated.count === 0) {
            throw new AppError("Payment attempt state changed, please refresh", 409)
        }

        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: attempt.provider,
                stationId: attempt.stationId,
                reason,
                source: "manual_resolution"
            },
            attempt.id,
            1
        )

        await paymentTerminalRealtimeService.requestRefresh({
            shopId: attempt.shopId,
            deviceId: attempt.providerDeviceId,
            paymentAttemptId: attempt.id,
            orderId: attempt.orderId,
            stationId: attempt.stationId,
            senderSocketId,
            reason,
        })

        return {
            attemptId: attempt.id,
            orderId: attempt.orderId,
            status: "FAILED",
            paymentId: null,
            failureReason: reason
        }
    }
}
