/**
 * Outbox repository: Prisma adapter. The only file in this module that knows
 * about `prisma`. The service depends on `OutboxRepository` (interface) so
 * tests can inject an in-memory mock without spinning up a database.
 */

import type { Prisma } from '@prisma/client';
import { prisma } from '@/lib/prisma';
import type { OutboxEntry, OutboxInput, OutboxChannelType } from './types';

export interface OutboxRepository {
    create(input: OutboxInput): Promise<OutboxEntry>;
    claimDue(limit: number, now: Date): Promise<OutboxEntry[]>;
    markDelivered(id: number, deliveredAt: Date): Promise<void>;
    markFailedAttempt(id: number, error: string, nextAttemptAt: Date): Promise<void>;
    markPermanentFailure(id: number, error: string, failedAt: Date): Promise<void>;
}

function toEntry(row: {
    id: number;
    eventKey: string;
    eventType: string;
    channelType: string;
    channelId: number | null;
    payload: string;
    attempts: number;
    maxAttempts: number;
    lastError: string | null;
    nextAttemptAt: Date;
    deliveredAt: Date | null;
    failedAt: Date | null;
    createdAt: Date;
}): OutboxEntry {
    return {
        id: row.id,
        eventKey: row.eventKey,
        eventType: row.eventType,
        channelType: row.channelType as OutboxChannelType,
        channelId: row.channelId,
        payload: row.payload,
        attempts: row.attempts,
        maxAttempts: row.maxAttempts,
        lastError: row.lastError,
        nextAttemptAt: row.nextAttemptAt,
        deliveredAt: row.deliveredAt,
        failedAt: row.failedAt,
        createdAt: row.createdAt,
    };
}

export class PrismaOutboxRepository implements OutboxRepository {
    async create(input: OutboxInput): Promise<OutboxEntry> {
        const row = await prisma.notificationOutbox.create({
            data: {
                eventKey: input.eventKey,
                eventType: input.eventType,
                channelType: input.channelType,
                channelId: input.channelId,
                payload: input.payload,
                maxAttempts: input.maxAttempts ?? 3,
                nextAttemptAt: input.nextAttemptAt ?? new Date(),
            },
        });
        return toEntry(row);
    }

    /**
     * Atomically claim up to `limit` due rows. Uses FOR UPDATE SKIP LOCKED so
     * multiple workers don't fight over the same row (single-worker today,
     * but the lock makes scaling out a non-event).
     */
    async claimDue(limit: number, now: Date): Promise<OutboxEntry[]> {
        return prisma.$transaction(async (tx) => {
            const locked = await tx.$queryRawUnsafe<{ id: number }[]>(
                `SELECT id FROM NotificationOutbox
                 WHERE deliveredAt IS NULL
                   AND failedAt IS NULL
                   AND nextAttemptAt <= ?
                 ORDER BY nextAttemptAt ASC
                 LIMIT ?
                 FOR UPDATE SKIP LOCKED`,
                now,
                limit
            );
            if (locked.length === 0) return [];
            const ids = locked.map((r) => r.id);

            await tx.notificationOutbox.updateMany({
                where: { id: { in: ids } },
                data: { attempts: { increment: 1 } },
            });

            const rows = await tx.notificationOutbox.findMany({
                where: { id: { in: ids } },
                orderBy: { nextAttemptAt: 'asc' },
            });
            return rows.map(toEntry);
        }, { isolationLevel: 'ReadCommitted' as Prisma.TransactionIsolationLevel });
    }

    async markDelivered(id: number, deliveredAt: Date): Promise<void> {
        await prisma.notificationOutbox.update({
            where: { id },
            data: { deliveredAt, lastError: null },
        });
    }

    async markFailedAttempt(id: number, error: string, nextAttemptAt: Date): Promise<void> {
        await prisma.notificationOutbox.update({
            where: { id },
            data: { lastError: error, nextAttemptAt },
        });
    }

    async markPermanentFailure(id: number, error: string, failedAt: Date): Promise<void> {
        await prisma.notificationOutbox.update({
            where: { id },
            data: { lastError: error, failedAt },
        });
    }
}
