import { AppError } from "@/core/AppError"
import { prisma } from "@/core/prisma"
import { Prisma } from "@prisma/client"
import { listPaymentProviderDefinitions } from "./providers/payment-provider.registry"
import {
    PAYMENT_METHOD_LABELS,
    type PaymentMethod,
} from "ttm-shared"
import type {
    CreateShopPaymentMethodConfigInput,
    CreateStationPaymentMethodConfigInput,
    PaymentConfigMetaDto,
    ShopPaymentMethodConfigDto,
    StationPaymentMethodConfigDto,
    UpdateShopPaymentMethodConfigInput,
    UpdateStationPaymentMethodConfigInput
} from "ttm-shared"

const OFFLINE_METHODS = new Set(["CASH", "GIFT_CARD", "VOUCHER"])

function toMetadataRecord(value: unknown): Record<string, unknown> {
    if (typeof value !== "object" || value === null || Array.isArray(value)) return {}
    return { ...(value as Record<string, unknown>) }
}

function normalizeCustomLabel(value: string | null | undefined): string | null | undefined {
    if (value === undefined) return undefined
    if (value === null) return null
    const trimmed = value.trim()
    return trimmed.length > 0 ? trimmed : null
}

function readCustomLabel(metadata: unknown): string | null {
    const candidate = toMetadataRecord(metadata).customLabel
    return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : null
}

function buildPaymentMethodDisplayLabel(method: PaymentMethod, customLabel: string | null): string {
    return customLabel ?? PAYMENT_METHOD_LABELS[method] ?? method
}

function buildShopConfigMetadataPatch(
    existingMetadata: unknown,
    customLabel: string | null | undefined,
): Prisma.InputJsonValue | typeof Prisma.DbNull | undefined {
    if (customLabel === undefined) return undefined

    const metadata = toMetadataRecord(existingMetadata)
    if (customLabel === null) {
        delete metadata.customLabel
    } else {
        metadata.customLabel = customLabel
    }

    return Object.keys(metadata).length > 0
        ? metadata as Prisma.InputJsonValue
        : Prisma.DbNull
}

function ensureCustomLabelCompatibility(
    method: PaymentMethod,
    accountId: string | null | undefined,
    deviceId: string | null | undefined,
    customLabel: string | null | undefined,
) {
    if (customLabel === undefined) return
    if (accountId || deviceId) {
        throw new AppError("Custom labels are only supported for global payment methods", 400)
    }

    if (method === "CARD" || method === "APPLE_PAY" || method === "GOOGLE_PAY") {
        throw new AppError("Custom labels are not supported for card payment methods", 400)
    }
}

function assertStandardCashDrawerImpactMode(cashDrawerImpactMode: string | null | undefined) {
    if (cashDrawerImpactMode === "DECREASE") {
        throw new AppError("DECREASE is reserved for advanced cash drawer flows", 400)
    }
}

function toShopConfigDto(row: any): ShopPaymentMethodConfigDto {
    const customLabel = readCustomLabel(row.metadata)
    return {
        id: row.id,
        shopId: row.shopId,
        method: row.method,
        customLabel,
        displayLabel: buildPaymentMethodDisplayLabel(row.method, customLabel),
        enabled: row.enabled,
        displayOrder: row.displayOrder,
        revision: row.revision,
        accountPaymentProviderId: row.accountPaymentProviderId ?? null,
        accountPaymentProviderLabel: row.accountPaymentProvider?.label ?? null,
        provider: row.accountPaymentProvider?.provider ?? null,
        defaultProviderDeviceId: row.defaultProviderDeviceId ?? null,
        defaultProviderDeviceLabel: row.defaultProviderDevice?.label ?? null,
        cashDrawerImpactMode: row.cashDrawerImpactMode ?? "NONE",
        createdAt: row.createdAt.toISOString(),
        updatedAt: row.updatedAt.toISOString()
    }
}

function toStationConfigDto(row: any): StationPaymentMethodConfigDto {
    const customLabel = readCustomLabel(row.shopPaymentMethodConfig?.metadata)
    return {
        id: row.id,
        stationId: row.stationId,
        stationName: row.station.name,
        shopPaymentMethodConfigId: row.shopPaymentMethodConfigId,
        method: row.shopPaymentMethodConfig.method,
        customLabel,
        displayLabel: buildPaymentMethodDisplayLabel(row.shopPaymentMethodConfig.method, customLabel),
        enabled: row.enabled,
        displayOrder: row.displayOrder,
        isDefault: row.isDefault,
        providerDeviceId: row.providerDeviceId ?? null,
        providerDeviceLabel: row.providerDevice?.label ?? null,
        createdAt: row.createdAt.toISOString(),
        updatedAt: row.updatedAt.toISOString()
    }
}

async function ensureProviderAccount(ownerId: string, accountId: string | null | undefined) {
    if (!accountId) return null

    const account = await prisma.accountPaymentProvider.findFirst({
        where: { id: accountId, ownerId }
    })

    if (!account) {
        throw new AppError("Provider account not found for this owner", 400)
    }

    return account
}

async function ensureProviderDevice(ownerId: string, deviceId: string | null | undefined) {
    if (!deviceId) return null

    const device = await prisma.providerDevice.findFirst({
        where: {
            id: deviceId,
            accountPaymentProvider: { ownerId }
        },
        include: {
            accountPaymentProvider: true
        }
    })

    if (!device) {
        throw new AppError("Provider device not found for this owner", 400)
    }

    return device
}

function ensureOfflineMethodCompatibility(method: string, accountId?: string | null, deviceId?: string | null) {
    if (OFFLINE_METHODS.has(method) && (accountId || deviceId)) {
        throw new AppError("Offline payment methods cannot use provider account or device", 400)
    }
}

function ensureAccountDeviceMatch(accountId: string | null | undefined, device: any | null) {
    if (!device) return
    if (!accountId) {
        throw new AppError("A provider device requires a provider account", 400)
    }
    if (device.accountPaymentProviderId !== accountId) {
        throw new AppError("Provider device does not belong to the selected provider account", 400)
    }
}

async function ensureShopConfigUniqueness(args: {
    shopId: string
    method: string
    accountPaymentProviderId: string | null
    excludeConfigId?: string
}) {
    const duplicate = await prisma.shopPaymentMethodConfig.findFirst({
        where: {
            shopId: args.shopId,
            method: args.method as any,
            accountPaymentProviderId: args.accountPaymentProviderId,
            ...(args.excludeConfigId ? { id: { not: args.excludeConfigId } } : {})
        }
    })

    if (!duplicate) return

    if (args.accountPaymentProviderId) {
        throw new AppError("This payment method is already configured for the selected provider account", 400)
    }

    throw new AppError("This global payment method is already configured for the shop", 400)
}

export const paymentConfigService = {
    async getMeta(shopId: string, ownerId: string): Promise<PaymentConfigMetaDto> {
        const [providerAccounts, providerDevices, stations] = await Promise.all([
            prisma.accountPaymentProvider.findMany({
                where: { ownerId, active: true },
                orderBy: [{ provider: "asc" }, { label: "asc" }]
            }),
            prisma.providerDevice.findMany({
                where: { accountPaymentProvider: { ownerId }, active: true },
                include: { accountPaymentProvider: true },
                orderBy: [{ accountPaymentProviderId: "asc" }, { label: "asc" }]
            }),
            prisma.station.findMany({
                where: { shopId, active: true },
                orderBy: { name: "asc" }
            })
        ])

        return {
            providerAccounts: providerAccounts.map((row) => ({
                id: row.id,
                label: row.label ?? null,
                provider: row.provider,
                active: row.active
            })),
            providerDevices: providerDevices.map((row) => ({
                id: row.id,
                label: row.label ?? null,
                externalId: row.externalId,
                active: row.active,
                providerAccountId: row.accountPaymentProviderId,
                provider: row.accountPaymentProvider.provider
            })),
            stations: stations.map((row) => ({
                id: row.id,
                name: row.name
            })),
            providerDefinitions: listPaymentProviderDefinitions()
        }
    },

    async listShopConfigs(shopId: string): Promise<ShopPaymentMethodConfigDto[]> {
        const rows = await prisma.shopPaymentMethodConfig.findMany({
            where: { shopId },
            include: {
                accountPaymentProvider: true,
                defaultProviderDevice: true
            },
            orderBy: [{ displayOrder: "asc" }, { method: "asc" }]
        })

        return rows.map(toShopConfigDto)
    },

    async createShopConfig(
        shopId: string,
        ownerId: string,
        input: CreateShopPaymentMethodConfigInput
    ): Promise<ShopPaymentMethodConfigDto> {
        assertStandardCashDrawerImpactMode(input.cashDrawerImpactMode)
        ensureOfflineMethodCompatibility(input.method, input.accountPaymentProviderId, input.defaultProviderDeviceId)
        const normalizedCustomLabel = normalizeCustomLabel(input.customLabel)
        ensureCustomLabelCompatibility(input.method, input.accountPaymentProviderId, input.defaultProviderDeviceId, normalizedCustomLabel)
        const account = await ensureProviderAccount(ownerId, input.accountPaymentProviderId)
        const device = await ensureProviderDevice(ownerId, input.defaultProviderDeviceId)
        ensureAccountDeviceMatch(account?.id ?? null, device)

        await ensureShopConfigUniqueness({
            shopId,
            method: input.method,
            accountPaymentProviderId: account?.id ?? null
        })

        const createData: Prisma.ShopPaymentMethodConfigUncheckedCreateInput = {
            shopId,
            method: input.method,
            enabled: input.enabled,
            displayOrder: input.displayOrder,
            accountPaymentProviderId: account?.id ?? null,
            defaultProviderDeviceId: device?.id ?? null,
            cashDrawerImpactMode: input.cashDrawerImpactMode,
            ...(normalizedCustomLabel !== undefined ? { metadata: buildShopConfigMetadataPatch(null, normalizedCustomLabel) } : {}),
        }

        const created = await prisma.shopPaymentMethodConfig.create({
            data: createData,
            include: {
                accountPaymentProvider: true,
                defaultProviderDevice: true
            }
        })

        return toShopConfigDto(created)
    },

    async updateShopConfig(
        shopId: string,
        ownerId: string,
        configId: string,
        input: UpdateShopPaymentMethodConfigInput
    ): Promise<ShopPaymentMethodConfigDto> {
        assertStandardCashDrawerImpactMode(input.cashDrawerImpactMode)
        const existing = await prisma.shopPaymentMethodConfig.findFirst({
            where: { id: configId, shopId }
        })

        if (!existing) {
            throw new AppError("Shop payment method config not found", 404)
        }

        const accountId = input.accountPaymentProviderId !== undefined
            ? input.accountPaymentProviderId
            : existing.accountPaymentProviderId
        const deviceId = input.defaultProviderDeviceId !== undefined
            ? input.defaultProviderDeviceId
            : existing.defaultProviderDeviceId
        const normalizedCustomLabel = normalizeCustomLabel(input.customLabel)

        ensureOfflineMethodCompatibility(existing.method, accountId, deviceId)
        ensureCustomLabelCompatibility(existing.method, accountId, deviceId, normalizedCustomLabel)
        const account = await ensureProviderAccount(ownerId, accountId)
        const device = await ensureProviderDevice(ownerId, deviceId)
        ensureAccountDeviceMatch(account?.id ?? null, device)
        await ensureShopConfigUniqueness({
            shopId,
            method: existing.method,
            accountPaymentProviderId: account?.id ?? null,
            excludeConfigId: configId
        })

        const updateData: Prisma.ShopPaymentMethodConfigUncheckedUpdateInput = {
            ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
            ...(input.displayOrder !== undefined ? { displayOrder: input.displayOrder } : {}),
            ...(input.accountPaymentProviderId !== undefined ? { accountPaymentProviderId: account?.id ?? null } : {}),
            ...(input.defaultProviderDeviceId !== undefined ? { defaultProviderDeviceId: device?.id ?? null } : {}),
            ...(input.cashDrawerImpactMode !== undefined ? { cashDrawerImpactMode: input.cashDrawerImpactMode } : {}),
            ...(normalizedCustomLabel !== undefined ? { metadata: buildShopConfigMetadataPatch(existing.metadata, normalizedCustomLabel) } : {}),
            revision: { increment: 1 }
        }

        const updated = await prisma.shopPaymentMethodConfig.update({
            where: { id: configId },
            data: updateData,
            include: {
                accountPaymentProvider: true,
                defaultProviderDevice: true
            }
        })

        return toShopConfigDto(updated)
    },

    async deleteShopConfig(shopId: string, configId: string) {
        const existing = await prisma.shopPaymentMethodConfig.findFirst({
            where: { id: configId, shopId }
        })

        if (!existing) {
            throw new AppError("Shop payment method config not found", 404)
        }

        await prisma.shopPaymentMethodConfig.delete({
            where: { id: configId }
        })
    },

    async listStationConfigsForShop(shopId: string): Promise<StationPaymentMethodConfigDto[]> {
        const rows = await prisma.stationPaymentMethodConfig.findMany({
            where: { station: { shopId } },
            include: {
                station: true,
                shopPaymentMethodConfig: true,
                providerDevice: true
            },
            orderBy: [
                { station: { name: "asc" } },
                { displayOrder: "asc" }
            ]
        })

        return rows.map(toStationConfigDto)
    },

    async createStationConfig(
        shopId: string,
        ownerId: string,
        input: CreateStationPaymentMethodConfigInput
    ): Promise<StationPaymentMethodConfigDto> {
        const station = await prisma.station.findFirst({ where: { id: input.stationId, shopId } })
        if (!station) {
            throw new AppError("Station not found for this shop", 400)
        }

        const shopConfig = await prisma.shopPaymentMethodConfig.findFirst({
            where: { id: input.shopPaymentMethodConfigId, shopId }
        })
        if (!shopConfig) {
            throw new AppError("Shop payment method config not found for this shop", 400)
        }

        ensureOfflineMethodCompatibility(shopConfig.method, shopConfig.accountPaymentProviderId, input.providerDeviceId)
        const device = await ensureProviderDevice(ownerId, input.providerDeviceId)
        ensureAccountDeviceMatch(shopConfig.accountPaymentProviderId, device)

        const existing = await prisma.stationPaymentMethodConfig.findUnique({
            where: {
                stationId_shopPaymentMethodConfigId: {
                    stationId: input.stationId,
                    shopPaymentMethodConfigId: input.shopPaymentMethodConfigId
                }
            }
        })

        if (existing) {
            throw new AppError("This station already has an override for the selected payment method", 400)
        }

        const created = await prisma.stationPaymentMethodConfig.create({
            data: {
                stationId: input.stationId,
                shopPaymentMethodConfigId: input.shopPaymentMethodConfigId,
                enabled: input.enabled,
                displayOrder: input.displayOrder,
                isDefault: input.isDefault,
                providerDeviceId: device?.id ?? null
            },
            include: {
                station: true,
                shopPaymentMethodConfig: true,
                providerDevice: true
            }
        })

        return toStationConfigDto(created)
    },

    async updateStationConfig(
        shopId: string,
        ownerId: string,
        configId: string,
        input: UpdateStationPaymentMethodConfigInput
    ): Promise<StationPaymentMethodConfigDto> {
        const existing = await prisma.stationPaymentMethodConfig.findFirst({
            where: { id: configId, station: { shopId } },
            include: {
                shopPaymentMethodConfig: true
            }
        })

        if (!existing) {
            throw new AppError("Station payment method config not found", 404)
        }

        const deviceId = input.providerDeviceId !== undefined
            ? input.providerDeviceId
            : existing.providerDeviceId

        ensureOfflineMethodCompatibility(existing.shopPaymentMethodConfig.method, existing.shopPaymentMethodConfig.accountPaymentProviderId, deviceId)
        const device = await ensureProviderDevice(ownerId, deviceId)
        ensureAccountDeviceMatch(existing.shopPaymentMethodConfig.accountPaymentProviderId, device)

        const updated = await prisma.stationPaymentMethodConfig.update({
            where: { id: configId },
            data: {
                ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
                ...(input.displayOrder !== undefined ? { displayOrder: input.displayOrder } : {}),
                ...(input.isDefault !== undefined ? { isDefault: input.isDefault } : {}),
                ...(input.providerDeviceId !== undefined ? { providerDeviceId: device?.id ?? null } : {})
            },
            include: {
                station: true,
                shopPaymentMethodConfig: true,
                providerDevice: true
            }
        })

        return toStationConfigDto(updated)
    },

    async deleteStationConfig(shopId: string, configId: string) {
        const existing = await prisma.stationPaymentMethodConfig.findFirst({
            where: { id: configId, station: { shopId } }
        })

        if (!existing) {
            throw new AppError("Station payment method config not found", 404)
        }

        await prisma.stationPaymentMethodConfig.delete({
            where: { id: configId }
        })
    }
}
