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

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

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 {
    return {
        id: row.id,
        shopId: row.shopId,
        method: row.method,
        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 {
    return {
        id: row.id,
        stationId: row.stationId,
        stationName: row.station.name,
        shopPaymentMethodConfigId: row.shopPaymentMethodConfigId,
        method: row.shopPaymentMethodConfig.method,
        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 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 created = await prisma.shopPaymentMethodConfig.create({
            data: {
                shopId,
                method: input.method,
                enabled: input.enabled,
                displayOrder: input.displayOrder,
                accountPaymentProviderId: account?.id ?? null,
                defaultProviderDeviceId: device?.id ?? null,
                cashDrawerImpactMode: input.cashDrawerImpactMode,
            },
            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

        ensureOfflineMethodCompatibility(existing.method, accountId, deviceId)
        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 updated = await prisma.shopPaymentMethodConfig.update({
            where: { id: configId },
            data: {
                ...(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 } : {}),
                revision: { increment: 1 }
            },
            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 }
        })
    }
}
