import { AppError } from "@/core/AppError"
import { createHash } from "crypto"
import { PaymentProvider } from "@prisma/client"
import {
    PAYMENT_PROVIDER_DEFINITIONS,
    type PaymentProviderAccountEnvironment,
    type PaymentProviderAccountMetadataDto,
    type PaymentProviderAccountSummaryDto,
    type PaymentProviderConnectionDiagnosticDto,
    type PaymentProviderDiagnosticRawDto,
    type PaymentProviderInputMap,
    type PaymentProviderInputValue,
    type PaymentProviderRemoteDeviceDto,
    type PaymentProviderTerminalDiagnosticDto,
} from "ttm-shared"
import type {
    PaymentProviderAdapter,
    ProviderAccountRecordLike,
    ProviderDeviceRecordLike,
    ProviderDiagnosticOptions,
    ProviderPendingAttemptLike,
    ValidateProviderAccountPayloadArgs,
    ValidatedProviderAccountPayload,
} from "./payment-provider.registry"

const definition = PAYMENT_PROVIDER_DEFINITIONS[PaymentProvider.STRIPE]

const STRIPE_API_BASE = "https://api.stripe.com/v1"
const STRIPE_DIAGNOSTIC_TIMEOUT_MS = 5_000

interface StripeResolvedAccountConfig {
    publishableKey: string | null
    webhookSecret: string | null
}

interface StripeResolvedAccountSecrets {
    secretKey: string
}

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

function requireString(map: PaymentProviderInputMap, key: string): string {
    const value = map[key]
    if (typeof value !== "string" || value.trim().length === 0) {
        throw new AppError(`Missing Stripe field: ${key}`, 400)
    }
    return value.trim()
}

function optionalString(map: PaymentProviderInputMap, key: string): string | null {
    const value = map[key]
    if (typeof value !== "string") return null
    const trimmed = value.trim()
    return trimmed.length > 0 ? trimmed : null
}

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 toRecord(value: unknown): Record<string, unknown> | null {
    if (typeof value !== "object" || value === null || Array.isArray(value)) return null
    return value as Record<string, unknown>
}

function firstNonEmptyString(...candidates: unknown[]): string | null {
    for (const candidate of candidates) {
        if (typeof candidate === "string" && candidate.trim().length > 0) {
            return candidate.trim()
        }
    }

    return null
}

function toHeadersRecord(headers: Headers): Record<string, string> {
    const collected: Record<string, string> = {}
    headers.forEach((value, key) => {
        collected[key] = value
    })
    return collected
}

function maskSensitiveHeader(key: string, value: string): string {
    const normalizedKey = key.toLowerCase()
    if (normalizedKey === "authorization") {
        return /^Bearer\s+/iu.test(value) ? "Bearer ***" : "***"
    }

    return value
}

function buildDiagnosticRaw(options: {
    includeRaw?: boolean
    request?: {
        method: string
        url: string
        headers?: Record<string, string>
        body?: unknown
    } | null
    response?: {
        ok: boolean | null
        status: number | null
        headers?: Record<string, string>
        body?: unknown
    } | null
    notes?: string[]
}): PaymentProviderDiagnosticRawDto | null {
    if (!options.includeRaw) return null

    return {
        request: options.request ? {
            method: options.request.method,
            url: options.request.url,
            headers: Object.fromEntries(
                Object.entries(options.request.headers ?? {}).map(([key, value]) => [key, maskSensitiveHeader(key, value)])
            ),
            body: options.request.body ?? null,
        } : null,
        response: options.response ? {
            ok: options.response.ok,
            status: options.response.status,
            headers: options.response.headers ?? {},
            body: options.response.body ?? null,
        } : null,
        notes: options.notes ?? [],
    }
}

function buildTokenFingerprint(secretKey: string): string {
    return `sha256:${createHash("sha256").update(secretKey).digest("hex").slice(0, 12)}`
}

function buildStripeHeaders(secretKey: string, accountId?: string | null): Record<string, string> {
    const headers: Record<string, string> = {
        Authorization: `Bearer ${secretKey}`,
    }

    if (accountId) {
        headers["Stripe-Account"] = accountId
    }

    return headers
}

async function fetchStripeJson(args: {
    url: string
    secretKey: string
    accountId?: string | null
}): Promise<{ ok: boolean; status: number; body: unknown; headers: Record<string, string> }> {
    const controller = new AbortController()
    const timeout = setTimeout(() => controller.abort(), STRIPE_DIAGNOSTIC_TIMEOUT_MS)

    try {
        const response = await fetch(args.url, {
            method: "GET",
            headers: buildStripeHeaders(args.secretKey, args.accountId),
            signal: controller.signal,
        })

        let body: unknown = null
        try {
            body = await response.json()
        } catch {
            body = null
        }

        return {
            ok: response.ok,
            status: response.status,
            body,
            headers: toHeadersRecord(response.headers),
        }
    } finally {
        clearTimeout(timeout)
    }
}

function buildStripeAccountUrl(): string {
    return `${STRIPE_API_BASE}/account`
}

function buildStripeReadersUrl(cursor?: string | null): string {
    const query = new URLSearchParams({ limit: "100" })
    if (cursor) {
        query.set("starting_after", cursor)
    }

    return `${STRIPE_API_BASE}/terminal/readers?${query.toString()}`
}

function buildStripeReaderUrl(readerId: string): string {
    return `${STRIPE_API_BASE}/terminal/readers/${encodeURIComponent(readerId)}`
}

function toStripeEnvironmentFromLivemode(value: unknown): PaymentProviderAccountEnvironment | null {
    if (value === true) return "live"
    if (value === false) return "test"
    return null
}

function inferStripeEnvironmentFromSecretKey(secretKey: string): PaymentProviderAccountEnvironment | null {
    if (/^sk_test_/iu.test(secretKey)) return "test"
    if (/^sk_live_/iu.test(secretKey)) return "live"
    if (/^rk_test_/iu.test(secretKey)) return "test"
    if (/^rk_live_/iu.test(secretKey)) return "live"
    return null
}

function stripeEnvironmentLabel(environment: PaymentProviderAccountEnvironment | null): string | null {
    if (environment === "test") return "Mode test Stripe"
    if (environment === "live") return "Mode live Stripe"
    return null
}

function extractStripeAccountMetadata(
    payload: unknown,
    secretKey: string,
): Omit<PaymentProviderAccountMetadataDto, "provider" | "schemaVersion" | "refreshedAt"> {
    const asObject = toRecord(payload)
    const businessProfile = toRecord(asObject?.business_profile)
    const company = toRecord(asObject?.company)
    const environment = toStripeEnvironmentFromLivemode(asObject?.livemode) ?? inferStripeEnvironmentFromSecretKey(secretKey)

    return {
        environment,
        companyName: firstNonEmptyString(
            businessProfile?.name,
            company?.name,
            asObject?.display_name,
            asObject?.id,
        ),
        email: firstNonEmptyString(asObject?.email),
        remoteMerchantCode: firstNonEmptyString(asObject?.id),
        website: firstNonEmptyString(businessProfile?.url),
        city: firstNonEmptyString(company?.address_city),
        country: firstNonEmptyString(company?.address_country, asObject?.country),
    }
}

function toStripeRemoteReader(payload: unknown): PaymentProviderRemoteDeviceDto | null {
    const asObject = toRecord(payload)
    if (!asObject) return null

    const externalId = firstNonEmptyString(asObject.id)
    if (!externalId) return null

    const action = toRecord(asObject.action)
    const actionStatus = firstNonEmptyString(action?.status)
    const status = firstNonEmptyString(asObject.status)

    return {
        externalId,
        label: firstNonEmptyString(asObject.label, asObject.serial_number, asObject.id),
        model: firstNonEmptyString(asObject.device_type),
        serialNumber: firstNonEmptyString(asObject.serial_number),
        status: [status, actionStatus].filter((value): value is string => Boolean(value)).join(" / ") || null,
    }
}

async function listStripeRemoteReaders(account: {
    secretKey: string
}): Promise<PaymentProviderRemoteDeviceDto[]> {
    const readers: PaymentProviderRemoteDeviceDto[] = []
    const seen = new Set<string>()
    let cursor: string | null = null

    for (;;) {
        const url = buildStripeReadersUrl(cursor)
        const response = await fetchStripeJson({
            url,
            secretKey: account.secretKey,
        }).catch((error: unknown) => {
            throw new AppError(
                error instanceof Error && error.message.trim().length > 0
                    ? `Impossible de joindre l'API Stripe pour récupérer les terminaux: ${error.message.trim()}`
                    : "Impossible de joindre l'API Stripe pour récupérer les terminaux",
                502,
            )
        })

        if (!response.ok) {
            throw new AppError(`Impossible de récupérer les terminaux Stripe (${response.status})`, 502)
        }

        const body = toRecord(response.body)
        const items = Array.isArray(body?.data) ? body.data : []
        for (const item of items) {
            const reader = toStripeRemoteReader(item)
            if (!reader || seen.has(reader.externalId)) continue
            seen.add(reader.externalId)
            readers.push(reader)
        }

        if (body?.has_more !== true || items.length === 0) {
            break
        }

        const lastItem = toRecord(items[items.length - 1])
        cursor = firstNonEmptyString(lastItem?.id)
        if (!cursor) break
    }

    return readers
}

function buildStripeTerminalMessage(
    status: PaymentProviderTerminalDiagnosticDto["status"],
    pendingAttempt?: ProviderPendingAttemptLike | null,
    actionLabel?: string | null,
) {
    if (pendingAttempt) {
        return `Transaction locale en attente (${pendingAttempt.orderId})`
    }

    if (status === "ready") return "Terminal Stripe prêt et connecté"
    if (status === "offline") return "Terminal Stripe hors ligne"
    if (status === "busy") return actionLabel ? `Terminal Stripe occupé (${actionLabel})` : "Terminal Stripe occupé"
    return "Statut du terminal Stripe indisponible"
}

export const stripePaymentProviderAdapter: PaymentProviderAdapter & {
    resolveStripeAccount(account: ProviderAccountRecordLike): {
        config: StripeResolvedAccountConfig
        secrets: StripeResolvedAccountSecrets
    }
} = {
    provider: PaymentProvider.STRIPE,
    definition,
    validateAccountPayload(args: ValidateProviderAccountPayloadArgs): ValidatedProviderAccountPayload {
        const mergedConfig = {
            ...(args.existingConfig ?? {}),
            ...(args.config ?? {}),
        }
        const mergedSecrets = {
            ...(args.existingSecrets ?? {}),
            ...(args.secrets ?? {}),
        }

        const config: PaymentProviderInputMap = {
            publishableKey: normalizeStringValue(mergedConfig.publishableKey),
            webhookSecret: normalizeStringValue(mergedConfig.webhookSecret),
        }

        const secrets: PaymentProviderInputMap = {
            secretKey: normalizeStringValue(mergedSecrets.secretKey),
        }

        const isWriteMode = args.config !== undefined || args.secrets !== undefined
        if (isWriteMode && !secrets.secretKey) {
            throw new AppError("Le champ \"Secret key\" est requis pour Stripe", 400)
        }

        return { config, secrets }
    },
    readAccount(account: ProviderAccountRecordLike): ValidatedProviderAccountPayload {
        return this.validateAccountPayload({
            existingConfig: toInputMap(account.config),
            existingSecrets: toInputMap(account.secrets),
        })
    },
    getConfiguredSecretKeys(secrets: PaymentProviderInputMap) {
        return definition.fields
            .filter((field) => field.secret)
            .map((field) => field.key)
            .filter((key) => {
                const value = secrets[key]
                return value !== null && value !== undefined && String(value).trim().length > 0
            })
    },
    resolveStripeAccount(account: ProviderAccountRecordLike) {
        const resolved = this.readAccount(account)
        return {
            config: {
                publishableKey: optionalString(resolved.config, "publishableKey"),
                webhookSecret: optionalString(resolved.config, "webhookSecret"),
            },
            secrets: {
                secretKey: requireString(resolved.secrets, "secretKey"),
            },
        }
    },
    async getAccountMetadata(account: ProviderAccountRecordLike): Promise<PaymentProviderAccountMetadataDto | null> {
        const resolved = this.resolveStripeAccount(account)
        const response = await fetchStripeJson({
            url: buildStripeAccountUrl(),
            secretKey: resolved.secrets.secretKey,
        })

        if (!response.ok) {
            throw new AppError(`Impossible de récupérer les informations du compte Stripe (${response.status})`, 502)
        }

        return {
            provider: PaymentProvider.STRIPE,
            schemaVersion: 1,
            refreshedAt: new Date().toISOString(),
            ...extractStripeAccountMetadata(response.body, resolved.secrets.secretKey),
        }
    },
    getAccountSummary(args: {
        account: ProviderAccountRecordLike
        metadata: PaymentProviderAccountMetadataDto | null
    }): PaymentProviderAccountSummaryDto | null {
        const resolved = this.resolveStripeAccount(args.account)
        const location = [args.metadata?.city, args.metadata?.country].filter((value): value is string => Boolean(value)).join(", ")
        const environmentDetail = stripeEnvironmentLabel(args.metadata?.environment ?? null)
        const details = [
            environmentDetail,
            args.metadata?.companyName && args.metadata.companyName !== args.account.label ? args.metadata.companyName : null,
            args.metadata?.email,
            args.metadata?.website,
            location,
        ].filter((value): value is string => Boolean(value))

        return {
            title: args.account.label ?? args.metadata?.companyName ?? "Compte Stripe",
            subtitle: args.metadata?.remoteMerchantCode
                ? `Compte ${args.metadata.remoteMerchantCode}`
                : "Compte principal",
            details,
        }
    },
    async listRemoteDevices(account: ProviderAccountRecordLike): Promise<PaymentProviderRemoteDeviceDto[]> {
        const resolved = this.resolveStripeAccount(account)
        return listStripeRemoteReaders({
            secretKey: resolved.secrets.secretKey,
        })
    },
    async runConnectionDiagnostic(account: ProviderAccountRecordLike, options?: ProviderDiagnosticOptions): Promise<PaymentProviderConnectionDiagnosticDto> {
        const resolved = this.resolveStripeAccount(account)
        const requestUrl = buildStripeAccountUrl()
        const tokenFingerprint = buildTokenFingerprint(resolved.secrets.secretKey)
        const requestHeaders = buildStripeHeaders(resolved.secrets.secretKey)

        try {
            const response = await fetchStripeJson({
                url: requestUrl,
                secretKey: resolved.secrets.secretKey,
            })
            const body = toRecord(response.body)
            const remoteAccountId = firstNonEmptyString(body?.id)

            if (!response.ok) {
                return {
                    accountId: account.id ?? "",
                    provider: PaymentProvider.STRIPE,
                    ok: false,
                    message: `Connexion Stripe refusée (${response.status})`,
                    testedAt: new Date().toISOString(),
                    remoteStatusCode: response.status,
                    merchantCode: null,
                    remoteMerchantCode: remoteAccountId,
                    merchantCodeStatus: "unknown",
                    merchantCodeNote: null,
                    configuredAccountReferenceLabel: "Compte testé",
                    configuredAccountReferenceValue: "Compte porté par la clé secrète",
                    remoteAccountReferenceLabel: "Compte Stripe distant",
                    remoteAccountReferenceValue: remoteAccountId,
                    tokenFingerprint,
                    accountLabel: account.label ?? null,
                    raw: buildDiagnosticRaw({
                        includeRaw: options?.includeRaw,
                        request: {
                            method: "GET",
                            url: requestUrl,
                            headers: requestHeaders,
                        },
                        response: {
                            ok: response.ok,
                            status: response.status,
                            headers: response.headers,
                            body: response.body,
                        },
                        notes: [
                            "L'en-tête Authorization est masqué dans cette vue.",
                            "Le test utilise la secret key stockée sur ce provider Stripe.",
                            "Le diagnostic Stripe interroge toujours /v1/account pour éviter les erreurs de contexte liées à un accountId legacy.",
                        ],
                    }),
                }
            }

            return {
                accountId: account.id ?? "",
                provider: PaymentProvider.STRIPE,
                ok: true,
                message: remoteAccountId
                    ? `Connexion Stripe valide (compte ${remoteAccountId})`
                    : "Connexion Stripe valide",
                testedAt: new Date().toISOString(),
                remoteStatusCode: response.status,
                merchantCode: null,
                remoteMerchantCode: remoteAccountId,
                merchantCodeStatus: "unknown",
                merchantCodeNote: null,
                configuredAccountReferenceLabel: "Compte testé",
                configuredAccountReferenceValue: "Compte porté par la clé secrète",
                remoteAccountReferenceLabel: "Compte Stripe distant",
                remoteAccountReferenceValue: remoteAccountId,
                tokenFingerprint,
                accountLabel: account.label ?? null,
                raw: buildDiagnosticRaw({
                    includeRaw: options?.includeRaw,
                    request: {
                        method: "GET",
                        url: requestUrl,
                        headers: requestHeaders,
                    },
                    response: {
                        ok: response.ok,
                        status: response.status,
                        headers: response.headers,
                        body: response.body,
                    },
                    notes: [
                        "L'en-tête Authorization est masqué dans cette vue.",
                        "Le test utilise la secret key stockée sur ce provider Stripe.",
                        "Le diagnostic Stripe interroge toujours /v1/account pour éviter les erreurs de contexte liées à un accountId legacy.",
                    ],
                }),
            }
        } catch (error) {
            return {
                accountId: account.id ?? "",
                provider: PaymentProvider.STRIPE,
                ok: false,
                message: error instanceof Error && error.message.trim().length > 0
                    ? error.message.trim()
                    : "Impossible de joindre l'API Stripe",
                testedAt: new Date().toISOString(),
                remoteStatusCode: null,
                merchantCode: null,
                remoteMerchantCode: null,
                merchantCodeStatus: "unknown",
                merchantCodeNote: null,
                configuredAccountReferenceLabel: "Compte testé",
                configuredAccountReferenceValue: "Compte porté par la clé secrète",
                remoteAccountReferenceLabel: "Compte Stripe distant",
                remoteAccountReferenceValue: null,
                tokenFingerprint,
                accountLabel: account.label ?? null,
                raw: buildDiagnosticRaw({
                    includeRaw: options?.includeRaw,
                    request: {
                        method: "GET",
                        url: requestUrl,
                        headers: requestHeaders,
                    },
                    response: {
                        ok: null,
                        status: null,
                        body: {
                            error: error instanceof Error ? error.message : "Impossible de joindre l'API Stripe",
                        },
                    },
                    notes: [
                        "L'en-tête Authorization est masqué dans cette vue.",
                        "Le test utilise la secret key stockée sur ce provider Stripe.",
                        "Le diagnostic Stripe interroge toujours /v1/account pour éviter les erreurs de contexte liées à un accountId legacy.",
                    ],
                }),
            }
        }
    },
    async runTerminalDiagnostic(args: {
        account: ProviderAccountRecordLike
        device: ProviderDeviceRecordLike
        pendingAttempt?: ProviderPendingAttemptLike | null
        options?: ProviderDiagnosticOptions
    }): Promise<PaymentProviderTerminalDiagnosticDto> {
        const resolved = this.resolveStripeAccount(args.account)
        const requestUrl = buildStripeReaderUrl(args.device.externalId)
        const requestHeaders = buildStripeHeaders(resolved.secrets.secretKey)

        if (!args.device.active) {
            return {
                deviceId: args.device.id,
                accountId: args.account.id ?? "",
                provider: PaymentProvider.STRIPE,
                externalId: args.device.externalId,
                label: args.device.label,
                active: args.device.active,
                ok: false,
                connected: false,
                status: "offline",
                message: "Terminal inactif dans TTM",
                testedAt: new Date().toISOString(),
                providerDeviceStatus: null,
                providerScreenState: null,
                pendingAttemptId: args.pendingAttempt?.id ?? null,
                pendingOrderId: args.pendingAttempt?.orderId ?? null,
                raw: buildDiagnosticRaw({
                    includeRaw: args.options?.includeRaw,
                    notes: ["Aucune requête distante envoyée car le terminal est inactif dans TTM."],
                }),
            }
        }

        const remoteResponse = await fetchStripeJson({
            url: requestUrl,
            secretKey: resolved.secrets.secretKey,
        }).catch((error: unknown) => ({
            ok: null,
            status: null,
            body: {
                error: error instanceof Error ? error.message : "Impossible de vérifier le terminal Stripe",
            },
            headers: {},
        }))

        const body = toRecord(remoteResponse.body)
        const action = toRecord(body?.action)
        const providerDeviceStatus = firstNonEmptyString(body?.status)
        const actionType = firstNonEmptyString(action?.type)
        const actionStatus = firstNonEmptyString(action?.status)
        const providerScreenState = [actionType, actionStatus].filter((value): value is string => Boolean(value)).join(" / ") || null

        let remoteStatus: PaymentProviderTerminalDiagnosticDto["status"] = "unknown"
        if (remoteResponse.ok === true) {
            if (providerDeviceStatus === "offline") {
                remoteStatus = "offline"
            } else if (actionStatus && actionStatus !== "succeeded" && actionStatus !== "failed") {
                remoteStatus = "busy"
            } else if (providerDeviceStatus === "online") {
                remoteStatus = "ready"
            }
        } else if (remoteResponse.status === 404) {
            remoteStatus = "offline"
        }

        const effectiveStatus = args.pendingAttempt ? "busy" : remoteStatus
        const connected = args.pendingAttempt
            ? true
            : effectiveStatus !== "offline" && effectiveStatus !== "unknown"

        return {
            deviceId: args.device.id,
            accountId: args.account.id ?? "",
            provider: PaymentProvider.STRIPE,
            externalId: args.device.externalId,
            label: args.device.label,
            active: args.device.active,
            ok: effectiveStatus === "ready",
            connected,
            status: effectiveStatus,
            message: buildStripeTerminalMessage(effectiveStatus, args.pendingAttempt, providerScreenState),
            testedAt: new Date().toISOString(),
            providerDeviceStatus,
            providerScreenState,
            pendingAttemptId: args.pendingAttempt?.id ?? null,
            pendingOrderId: args.pendingAttempt?.orderId ?? null,
            raw: buildDiagnosticRaw({
                includeRaw: args.options?.includeRaw,
                request: {
                    method: "GET",
                    url: requestUrl,
                    headers: requestHeaders,
                },
                response: {
                    ok: remoteResponse.ok,
                    status: remoteResponse.status,
                    headers: remoteResponse.headers,
                    body: remoteResponse.body,
                },
                notes: args.pendingAttempt
                    ? [
                        "Une tentative locale PENDING force l'état final à OCCUPÉ même si le provider répond autrement.",
                        "L'en-tête Authorization est masqué dans cette vue.",
                    ]
                    : ["L'en-tête Authorization est masqué dans cette vue."],
            }),
        }
    },
}
