import { AppError } from "@/core/AppError"
import { prisma } from "@/core/prisma"
import { PaymentProvider } from "@prisma/client"
import type { Prisma } from "@prisma/client"
import type {
    CreatePaymentProviderAccountInput,
    CreatePaymentProviderDeviceInput,
    PaymentProviderAccountDto,
    PaymentProviderDeviceDto,
    UpdatePaymentProviderAccountInput,
    UpdatePaymentProviderDeviceInput
} from "ttm-shared"
import type { PaymentProviderInputMap } from "ttm-shared"
import { getPaymentProviderAdapter } from "./providers/payment-provider.registry"

function toInputMap(value: unknown): PaymentProviderInputMap {
    if (typeof value !== "object" || value === null || Array.isArray(value)) return {}
    return Object.entries(value).reduce<PaymentProviderInputMap>((acc, [key, raw]) => {
        if (raw === null || typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
            acc[key] = raw
        }
        return acc
    }, {})
}

function toProviderAccountDto(row: {
    id: string
    provider: PaymentProviderAccountDto["provider"]
    label: string | null
    config: Prisma.JsonValue | null
    secrets: Prisma.JsonValue | null
    active: boolean
    createdAt: Date
    updatedAt: Date
}): PaymentProviderAccountDto {
    const adapter = getPaymentProviderAdapter(row.provider)
    const resolved = adapter.readAccount({
        provider: row.provider as PaymentProvider,
        config: row.config,
        secrets: row.secrets
    })

    return {
        id: row.id,
        provider: row.provider,
        label: row.label,
        config: resolved.config,
        configuredSecrets: adapter.getConfiguredSecretKeys(resolved.secrets),
        active: row.active,
        createdAt: row.createdAt.toISOString(),
        updatedAt: row.updatedAt.toISOString()
    }
}

function toProviderDeviceDto(row: {
    id: string
    accountPaymentProviderId: string
    externalId: string
    label: string | null
    active: boolean
    accountPaymentProvider: {
        provider: PaymentProviderDeviceDto["provider"]
        label: string | null
    }
}): PaymentProviderDeviceDto {
    return {
        id: row.id,
        accountPaymentProviderId: row.accountPaymentProviderId,
        accountPaymentProviderLabel: row.accountPaymentProvider.label,
        provider: row.accountPaymentProvider.provider,
        externalId: row.externalId,
        label: row.label,
        active: row.active
    }
}

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

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

    if (!account) {
        throw new AppError("Provider account not found", 404)
    }

    return account
}

export const paymentProviderAdminService = {
    async listProviderAccounts(ownerId: string): Promise<PaymentProviderAccountDto[]> {
        const rows = await prisma.accountPaymentProvider.findMany({
            where: { ownerId },
            orderBy: [{ active: "desc" }, { provider: "asc" }, { label: "asc" }, { createdAt: "desc" }]
        })

        return rows.map((row) => toProviderAccountDto({
            ...row,
            config: row.config as Prisma.JsonValue | null,
            secrets: row.secrets as Prisma.JsonValue | null
        }))
    },

    async createProviderAccount(ownerId: string, input: CreatePaymentProviderAccountInput): Promise<PaymentProviderAccountDto> {
        const adapter = getPaymentProviderAdapter(input.provider as PaymentProvider)
        const validated = adapter.validateAccountPayload({
            config: input.config ?? {},
            secrets: input.secrets ?? {}
        })

        const created = await prisma.accountPaymentProvider.create({
            data: {
                ownerId,
                provider: input.provider as PaymentProvider,
                label: normalizeNullableString(input.label) ?? null,
                config: validated.config as Prisma.InputJsonValue,
                secrets: validated.secrets as Prisma.InputJsonValue,
                active: input.active
            }
        })

        return toProviderAccountDto({
            ...created,
            config: created.config as Prisma.JsonValue | null,
            secrets: created.secrets as Prisma.JsonValue | null
        })
    },

    async updateProviderAccount(ownerId: string, accountId: string, input: UpdatePaymentProviderAccountInput): Promise<PaymentProviderAccountDto> {
        const existing = await ensureProviderAccountOwnedBy(accountId, ownerId)
        const provider = (input.provider ?? existing.provider) as PaymentProvider
        const adapter = getPaymentProviderAdapter(provider)

        const validated = adapter.validateAccountPayload({
            config: input.config ?? {},
            secrets: input.secrets ?? {},
            existingConfig: toInputMap(existing.config),
            existingSecrets: toInputMap(existing.secrets)
        })

        const updated = await prisma.accountPaymentProvider.update({
            where: { id: accountId },
            data: {
                ...(input.provider !== undefined ? { provider } : {}),
                ...(input.label !== undefined ? { label: normalizeNullableString(input.label) ?? null } : {}),
                ...(input.active !== undefined ? { active: input.active } : {}),
                config: validated.config as Prisma.InputJsonValue,
                secrets: validated.secrets as Prisma.InputJsonValue
            }
        })

        return toProviderAccountDto({
            ...updated,
            config: updated.config as Prisma.JsonValue | null,
            secrets: updated.secrets as Prisma.JsonValue | null
        })
    },

    async deleteProviderAccount(ownerId: string, accountId: string): Promise<void> {
        const account = await prisma.accountPaymentProvider.findFirst({
            where: { id: accountId, ownerId },
            include: {
                providerDevices: { select: { id: true } },
                shopPaymentConfigs: { select: { id: true } },
                payments: { select: { id: true } },
                paymentAttempts: { select: { id: true } }
            }
        })

        if (!account) {
            throw new AppError("Provider account not found", 404)
        }

        if (account.providerDevices.length > 0) {
            throw new AppError("Cannot delete a provider account that still has devices", 400)
        }

        if (account.shopPaymentConfigs.length > 0 || account.payments.length > 0 || account.paymentAttempts.length > 0) {
            throw new AppError("Cannot delete a provider account that is still used by payment configurations or payments", 400)
        }

        await prisma.accountPaymentProvider.delete({
            where: { id: accountId }
        })
    },

    async listProviderDevices(ownerId: string): Promise<PaymentProviderDeviceDto[]> {
        const rows = await prisma.providerDevice.findMany({
            where: {
                accountPaymentProvider: { ownerId }
            },
            include: {
                accountPaymentProvider: {
                    select: {
                        provider: true,
                        label: true
                    }
                }
            },
            orderBy: [
                { active: "desc" },
                { accountPaymentProviderId: "asc" },
                { label: "asc" },
                { externalId: "asc" }
            ]
        })

        return rows.map(toProviderDeviceDto)
    },

    async createProviderDevice(ownerId: string, input: CreatePaymentProviderDeviceInput): Promise<PaymentProviderDeviceDto> {
        await ensureProviderAccountOwnedBy(input.accountPaymentProviderId, ownerId)

        try {
            const created = await prisma.providerDevice.create({
                data: {
                    accountPaymentProviderId: input.accountPaymentProviderId,
                    externalId: input.externalId.trim(),
                    label: normalizeNullableString(input.label) ?? null,
                    active: input.active
                },
                include: {
                    accountPaymentProvider: {
                        select: {
                            provider: true,
                            label: true
                        }
                    }
                }
            })

            return toProviderDeviceDto(created)
        } catch (error: unknown) {
            if (error instanceof Error && error.message.includes("Unique constraint")) {
                throw new AppError("A device with this external id already exists for the selected provider account", 400)
            }
            throw error
        }
    },

    async updateProviderDevice(ownerId: string, deviceId: string, input: UpdatePaymentProviderDeviceInput): Promise<PaymentProviderDeviceDto> {
        const existing = await prisma.providerDevice.findFirst({
            where: {
                id: deviceId,
                accountPaymentProvider: { ownerId }
            }
        })

        if (!existing) {
            throw new AppError("Provider device not found", 404)
        }

        const accountPaymentProviderId = input.accountPaymentProviderId ?? existing.accountPaymentProviderId
        await ensureProviderAccountOwnedBy(accountPaymentProviderId, ownerId)

        try {
            const updated = await prisma.providerDevice.update({
                where: { id: deviceId },
                data: {
                    ...(input.accountPaymentProviderId !== undefined ? { accountPaymentProviderId } : {}),
                    ...(input.externalId !== undefined ? { externalId: input.externalId.trim() } : {}),
                    ...(input.label !== undefined ? { label: normalizeNullableString(input.label) ?? null } : {}),
                    ...(input.active !== undefined ? { active: input.active } : {})
                },
                include: {
                    accountPaymentProvider: {
                        select: {
                            provider: true,
                            label: true
                        }
                    }
                }
            })

            return toProviderDeviceDto(updated)
        } catch (error: unknown) {
            if (error instanceof Error && error.message.includes("Unique constraint")) {
                throw new AppError("A device with this external id already exists for the selected provider account", 400)
            }
            throw error
        }
    },

    async deleteProviderDevice(ownerId: string, deviceId: string): Promise<void> {
        const device = await prisma.providerDevice.findFirst({
            where: {
                id: deviceId,
                accountPaymentProvider: { ownerId }
            },
            include: {
                defaultShopPaymentConfigs: { select: { id: true } },
                stationPaymentConfigs: { select: { id: true } },
                payments: { select: { id: true } },
                paymentAttempts: { select: { id: true } }
            }
        })

        if (!device) {
            throw new AppError("Provider device not found", 404)
        }

        if (
            device.defaultShopPaymentConfigs.length > 0 ||
            device.stationPaymentConfigs.length > 0 ||
            device.payments.length > 0 ||
            device.paymentAttempts.length > 0
        ) {
            throw new AppError("Cannot delete a provider device that is still used by payment configurations or payments", 400)
        }

        await prisma.providerDevice.delete({
            where: { id: deviceId }
        })
    }
}
