import { prisma } from "@/core/prisma"
import { io } from "@/server"
import type { PrismaClient } from "@prisma/client"

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/** Noms des modèles Prisma possédant un champ `revision` */
export type TrackedModelName =
    | "shop"
    | "order"
    | "orderItem"
    | "product"
    | "shopUser"
    | "cashSession"
    | "station"

/**
 * Client Prisma ou client transactionnel (subset minimal requis).
 * On utilise le type du runtime Prisma pour les deux cas.
 */
export type PrismaLike = Pick<
    PrismaClient,
    "shop" | "order" | "orderItem" | "product" | "shopUser" | "cashSession" | "station" | "shopEvent" | "shopEventSeq"
>

/** Options passées à `tracked()` pour construire un contexte. */
export interface TrackedCtxOptions {
    shopId: string
    /** Nom lisible de l'entité – utilisé dans le ShopEvent (ex: "Order") */
    entity: string
    /** Utilisateur à l'origine de la mutation */
    actorUserId?: string | null
    deviceId?: string | null
    /**
     * Socket.IO socket ID de l'émetteur.
     * Si fourni, cet émetteur ne recevra PAS l'événement WS (broadcast).
     */
    senderSocketId?: string | null
    /**
     * Si on est dans une `$transaction`, passer le `tx` ici.
     * Toutes les opérations Prisma utiliseront ce client.
     */
    tx?: PrismaLike
}

/**
 * Événement bufferisé, émis après le commit.
 */
interface BufferedEvent {
    id: string
    shopId: string
    seq: string
    type: string
    entity: string | null
    entityId: string | null
    revision: number | null
    payload: any
    createdAt: string
}

// ---------------------------------------------------------------------------
// Helpers internes
// ---------------------------------------------------------------------------

function toSnakeEvent(entity: string): string {
    // "CashSession" → "cash_session", "OrderItem" → "order_item"
    return entity
        .replace(/([a-z])([A-Z])/g, "$1_$2")
        .toLowerCase()
}

// Cache shopId → ownerId pour éviter des queries répétées
const shopOwnerCache = new Map<string, string>()

async function resolveShopOwner(shopId: string): Promise<string | null> {
    if (shopOwnerCache.has(shopId)) return shopOwnerCache.get(shopId)!
    const shop = await prisma.shop.findUnique({ where: { id: shopId }, select: { ownerId: true } })
    if (shop) {
        shopOwnerCache.set(shopId, shop.ownerId)
        return shop.ownerId
    }
    return null
}

function emitShopEvent(shopId: string, event: BufferedEvent, senderSocketId?: string | null) {
    const shopRoom = `shop:${shopId}`

    // Émettre dans la room du shop
    if (senderSocketId) {
        const senderSocket = io.sockets.sockets.get(senderSocketId)
        if (senderSocket) {
            console.log(`[ws] Broadcasting shop:event (excluding ${senderSocketId})`, event.type)
            senderSocket.broadcast.to(shopRoom).emit("shop:event", event)
        } else {
            io.to(shopRoom).emit("shop:event", event)
        }
    } else {
        console.log(`[ws] Emitting shop:event to room ${shopRoom}`, event.type)
        io.to(shopRoom).emit("shop:event", event)
    }

    // Émettre aussi dans la room admin de l'owner (pour le dashboard admin)
    resolveShopOwner(shopId).then(ownerId => {
        if (ownerId) {
            const adminRoom = `admin:${ownerId}`
            console.log(`[ws] Emitting shop:event to admin room ${adminRoom}`, event.type)
            // Toujours émettre à l'admin (même s'il est l'émetteur HTTP,
            // car la room admin sert pour le dashboard global, pas la vue shop)
            io.to(adminRoom).emit("shop:event", event)
        }
    })
}

// ---------------------------------------------------------------------------
// tracked() — factory
// ---------------------------------------------------------------------------

/**
 * Crée un contexte de tracking pour un shopId + entité donnés.
 *
 * @example
 * // Cas simple (hors transaction)
 * const t = tracked({ shopId, entity: "Product", actorUserId: userId })
 * const product = await t.create("product", { ...data, shopId }, { include: { category: true } })
 *
 * @example
 * // Cas transaction
 * const result = await tracked.transaction(shopId, "Order", async (t, tx) => {
 *     const order = await t.update("order", { id: orderId }, { status: "PAID" }, { include: { orderItems: true } })
 *     return order
 * }, { actorUserId: userId })
 */
export function tracked(ctx: TrackedCtxOptions) {
    const db = (ctx.tx ?? prisma) as any
    const pendingEvents: BufferedEvent[] = []

    /** Séquenceur transactionnel-safe */
    async function nextSeq(): Promise<bigint> {
        const seq = await db.shopEventSeq.upsert({
            where: { shopId: ctx.shopId },
            create: { shopId: ctx.shopId, seq: 1n },
            update: { seq: { increment: 1 } }
        })
        return seq.seq
    }

    /** Crée un ShopEvent (dans la même tx si applicable) */
    async function createShopEvent(
        type: string,
        payload: Record<string, any>,
        entityId: string,
        revision: number
    ): Promise<BufferedEvent> {
        const seq = await nextSeq()

        const event = await db.shopEvent.create({
            data: {
                shopId: ctx.shopId,
                seq,
                type,
                payload,
                entity: ctx.entity,
                entityId,
                revision,
                actorUserId: ctx.actorUserId ?? null,
                deviceId: ctx.deviceId ?? null
            }
        })

        const buffered: BufferedEvent = {
            id: event.id,
            shopId: event.shopId,
            seq: event.seq.toString(),
            type: event.type,
            entity: event.entity,
            entityId: event.entityId,
            revision: event.revision,
            payload: event.payload,
            createdAt: event.createdAt.toISOString()
        }

        // Si on est dans une tx, on bufferise l'émission WS
        if (ctx.tx) {
            pendingEvents.push(buffered)
        } else {
            emitShopEvent(ctx.shopId, buffered, ctx.senderSocketId)
        }

        return buffered
    }

    return {
        // expose pour flush post-transaction
        get pendingEvents() { return pendingEvents },

        /**
         * Flush les événements WS bufferisés (à appeler après le commit d'une transaction).
         */
        flush() {
            for (const evt of pendingEvents) {
                emitShopEvent(ctx.shopId, evt, ctx.senderSocketId)
            }
            pendingEvents.length = 0
        },

        /**
         * CREATE avec revision=1 automatique + ShopEvent
         *
         * @param model   - nom du delegate Prisma (ex: "order")
         * @param data    - données de création (revision sera injectée)
         * @param opts    - options Prisma (include, select…)
         * @param eventType - type d'événement (défaut: "entity.created")
         * @param eventPayload - payload custom (défaut: data sérialisé)
         */
        async create<T = any>(
            model: TrackedModelName,
            data: Record<string, any>,
            opts?: { include?: any; select?: any },
            eventType?: string,
            eventPayload?: Record<string, any>
        ): Promise<T> {
            const createData = { ...data, revision: 1 }

            const result = await db[model].create({
                data: createData,
                ...opts
            })

            const type = eventType ?? `${toSnakeEvent(ctx.entity)}.created`
            const basePayload = eventPayload ?? { ...stripInternal(data) }
            // Toujours injecter l'id de l'entité dans le payload
            const payload = { [`${model}Id`]: result.id, ...basePayload }

            await createShopEvent(type, payload, result.id, 1)

            return result as T
        },

        /**
         * UPDATE avec revision incrémentée automatiquement + ShopEvent
         *
         * @param model   - nom du delegate Prisma (ex: "order")
         * @param where   - clause where Prisma
         * @param data    - données de mise à jour (revision sera incrémentée)
         * @param opts    - options Prisma (include, select…)
         * @param eventType - type d'événement (défaut: "entity.updated")
         * @param eventPayload - payload custom
         */
        async update<T = any>(
            model: TrackedModelName,
            where: Record<string, any>,
            data: Record<string, any>,
            opts?: { include?: any; select?: any },
            eventType?: string,
            eventPayload?: Record<string, any>
        ): Promise<T> {
            const updateData = { ...data, revision: { increment: 1 } }

            const result = await db[model].update({
                where,
                data: updateData,
                ...opts
            })

            const type = eventType ?? `${toSnakeEvent(ctx.entity)}.updated`
            const entityId = result.id ?? where.id
            const basePayload = eventPayload ?? {
                changes: Object.keys(data).filter(k => k !== "shopId")
            }
            // Toujours injecter l'id de l'entité dans le payload
            const payload = { [`${model}Id`]: entityId, ...basePayload }

            await createShopEvent(type, payload, entityId, result.revision)

            return result as T
        },

        /**
         * Émet manuellement un ShopEvent (mutation Prisma déjà faite).
         * Utile pour les cas complexes (multi-entités, delete, etc.)
         */
        async emit(
            eventType: string,
            payload: Record<string, any>,
            entityId: string,
            revision: number
        ): Promise<BufferedEvent> {
            return createShopEvent(eventType, payload, entityId, revision)
        }
    }
}

// ---------------------------------------------------------------------------
// tracked.transaction() — helper transactionnel
// ---------------------------------------------------------------------------

type TrackedInstance = ReturnType<typeof tracked>

interface TransactionOptions {
    actorUserId?: string | null
    deviceId?: string | null
    senderSocketId?: string | null
}

/**
 * Ouvre une transaction Prisma, crée un contexte tracked avec le `tx`,
 * exécute le callback, puis flush les événements WS après le commit.
 *
 * @example
 * const order = await trackedTransaction(shopId, "Order", async (t, tx) => {
 *     // t.update, t.create, t.emit... tous utilisent `tx`
 *     return await t.update("order", { id: orderId }, { status: "PAID" })
 * }, { actorUserId: userId })
 */
export async function trackedTransaction<T>(
    shopId: string,
    entity: string,
    fn: (t: TrackedInstance, tx: PrismaLike) => Promise<T>,
    options?: TransactionOptions
): Promise<T> {
    let tracker: TrackedInstance | null = null

    const result = await prisma.$transaction(async (tx) => {
        tracker = tracked({
            shopId,
            entity,
            actorUserId: options?.actorUserId,
            deviceId: options?.deviceId,
            senderSocketId: options?.senderSocketId,
            tx: tx as unknown as PrismaLike
        })

        return fn(tracker, tx as unknown as PrismaLike)
    })

    // Flush WS events après commit réussi
    tracker!.flush()

    return result
}

// ---------------------------------------------------------------------------
// Utilitaires
// ---------------------------------------------------------------------------

/** Retire les clés internes Prisma des données pour le payload d'événement */
function stripInternal(data: Record<string, any>): Record<string, any> {
    const { revision, createdAt, updatedAt, ...rest } = data
    return rest
}
