"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var NotificationService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NotificationService = void 0;
const api_pagination_1 = require("@hed-hog/api-pagination");
const api_prisma_1 = require("@hed-hog/api-prisma");
const common_1 = require("@nestjs/common");
const node_crypto_1 = require("node:crypto");
const node_events_1 = require("node:events");
const pg_1 = require("pg");
let NotificationService = NotificationService_1 = class NotificationService {
    constructor(prisma, pagination) {
        this.prisma = prisma;
        this.pagination = pagination;
        this.logger = new common_1.Logger(NotificationService_1.name);
        this.notificationEvents = new node_events_1.EventEmitter();
        this.instanceId = (0, node_crypto_1.randomUUID)();
        this.channelName = 'notification_events';
        this.pgClient = null;
        this.notificationEvents.setMaxListeners(0);
    }
    async onModuleInit() {
        const databaseUrl = String(process.env.DATABASE_URL || '');
        if (!databaseUrl.startsWith('postgresql://')) {
            this.logger.warn('Cluster notification stream disabled because DATABASE_URL is not PostgreSQL');
            return;
        }
        this.pgClient = new pg_1.Client({ connectionString: databaseUrl });
        this.pgClient.on('notification', (message) => {
            this.handleClusterNotification(message);
        });
        this.pgClient.on('error', (error) => {
            this.logger.error('Cluster notification listener error', error);
        });
        await this.pgClient.connect();
        await this.pgClient.query(`LISTEN ${this.channelName}`);
        this.logger.log(`Cluster notification listener ready on channel ${this.channelName}`);
    }
    async onModuleDestroy() {
        if (!this.pgClient) {
            return;
        }
        try {
            await this.pgClient.query(`UNLISTEN ${this.channelName}`);
        }
        catch (error) {
            this.logger.warn('Failed to unlisten notification channel', error);
        }
        try {
            await this.pgClient.end();
        }
        finally {
            this.pgClient = null;
        }
    }
    async list(userId, paginationParams) {
        return this.pagination.paginate(this.prisma.notification, paginationParams, {
            where: { user_id: userId },
            orderBy: { created_at: 'desc' },
        });
    }
    async unreadCount(userId) {
        const count = await this.prisma.notification.count({
            where: { user_id: userId, status: 'unread' },
        });
        return { count };
    }
    async markRead(userId, id) {
        const notification = await this.assertOwnership(userId, id);
        if (notification.auto_remove) {
            if (this.isActiveAsyncNotification(notification)) {
                throw new common_1.BadRequestException('Active async notifications cannot be deleted');
            }
            const deleted = await this.prisma.notification.delete({ where: { id } });
            await this.broadcastNotificationEvent(userId, {
                type: 'notification.updated',
                notificationId: id,
            });
            return deleted;
        }
        const updated = await this.prisma.notification.update({
            where: { id },
            data: { status: 'read' },
        });
        await this.broadcastNotificationEvent(userId, {
            type: 'notification.updated',
            notificationId: id,
        });
        return updated;
    }
    async markAllRead(userId) {
        await this.prisma.notification.deleteMany({
            where: {
                user_id: userId,
                status: 'unread',
                auto_remove: true,
                OR: [
                    { progress: null },
                    { progress: { gte: 100 } },
                    { finished_at: { not: null } },
                ],
            },
        });
        const updated = await this.prisma.notification.updateMany({
            where: { user_id: userId, status: 'unread' },
            data: { status: 'read' },
        });
        await this.broadcastNotificationEvent(userId, { type: 'notification.updated' });
        return updated;
    }
    async delete(userId, id) {
        const notification = await this.assertOwnership(userId, id);
        if (this.isActiveAsyncNotification(notification)) {
            throw new common_1.BadRequestException('Active async notifications cannot be deleted');
        }
        const deleted = await this.prisma.notification.delete({ where: { id } });
        await this.broadcastNotificationEvent(userId, {
            type: 'notification.updated',
            notificationId: id,
        });
        return deleted;
    }
    async deleteAll(userId) {
        const result = await this.prisma.notification.deleteMany({
            where: {
                user_id: userId,
                OR: [
                    { progress: null },
                    { progress: { gte: 100 } },
                    { finished_at: { not: null } },
                ],
            },
        });
        await this.broadcastNotificationEvent(userId, { type: 'notification.updated' });
        return result;
    }
    async create(data) {
        var _a, _b, _c;
        const notification = await this.prisma.notification.create({
            data: {
                user_id: data.user_id,
                title: data.title,
                body: data.body,
                icon: data.icon,
                type: (_a = data.type) !== null && _a !== void 0 ? _a : 'info',
                action_type: (_b = data.action_type) !== null && _b !== void 0 ? _b : 'url',
                action_data: data.action_data,
                action_url: data.action_url,
                auto_remove: (_c = data.auto_remove) !== null && _c !== void 0 ? _c : false,
                progress: data.progress,
                started_at: data.started_at ? new Date(data.started_at) : undefined,
            },
        });
        await this.broadcastNotificationEvent(data.user_id, {
            type: 'notification.created',
            notificationId: notification.id,
            notification,
        });
        return notification;
    }
    async updateProgress(userId, id, dto) {
        await this.assertOwnership(userId, id);
        const finished = dto.progress >= 100;
        const notification = await this.prisma.notification.update({
            where: { id },
            data: {
                progress: dto.progress,
                body: dto.body,
                type: finished ? (dto.success === false ? 'error' : 'success') : undefined,
                finished_at: finished ? new Date() : undefined,
            },
        });
        await this.broadcastNotificationEvent(userId, {
            type: 'notification.updated',
            notificationId: id,
            notification,
        });
        return notification;
    }
    stream(userId, req, res) {
        res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
        res.setHeader('Cache-Control', 'no-cache, no-transform');
        res.setHeader('Connection', 'keep-alive');
        res.setHeader('X-Accel-Buffering', 'no');
        res.flushHeaders();
        const writeEvent = (event) => {
            if (res.writableEnded) {
                return;
            }
            res.write(`event: ${event.type}\n`);
            res.write(`data: ${JSON.stringify(event)}\n\n`);
        };
        const eventKey = this.getEventKey(userId);
        const handler = (event) => {
            writeEvent(event);
        };
        this.notificationEvents.on(eventKey, handler);
        writeEvent({ type: 'ready' });
        const heartbeat = setInterval(() => {
            if (!res.writableEnded) {
                res.write(': ping\n\n');
            }
        }, 25000);
        const cleanup = () => {
            clearInterval(heartbeat);
            this.notificationEvents.off(eventKey, handler);
        };
        req.on('close', cleanup);
        res.on('close', cleanup);
    }
    async clearOldNotifications(retentionDays, batchLimit) {
        return this.prisma.$executeRawUnsafe(`WITH del AS (
         SELECT id FROM notification
         WHERE created_at < NOW() - INTERVAL '${retentionDays} days'
         ORDER BY id
         LIMIT ${batchLimit}
       )
       DELETE FROM notification n
       USING del WHERE n.id = del.id`);
    }
    async assertOwnership(userId, id) {
        const notification = await this.prisma.notification.findUnique({
            where: { id },
            select: {
                user_id: true,
                auto_remove: true,
                progress: true,
                finished_at: true,
            },
        });
        if (!notification)
            throw new common_1.NotFoundException();
        if (notification.user_id !== userId)
            throw new common_1.ForbiddenException();
        return notification;
    }
    isActiveAsyncNotification(notification) {
        return (notification.progress != null &&
            notification.progress < 100 &&
            notification.finished_at == null);
    }
    async broadcastNotificationEvent(userId, event) {
        this.emitLocalNotificationEvent(userId, event);
        const payload = Object.assign(Object.assign({}, event), { userId, originInstanceId: this.instanceId });
        try {
            await this.prisma.$executeRaw `
        SELECT pg_notify(${this.channelName}, ${JSON.stringify(payload)})
      `;
        }
        catch (error) {
            this.logger.warn('Failed to broadcast notification event', error);
        }
    }
    getEventKey(userId) {
        return `notification:${userId}`;
    }
    emitLocalNotificationEvent(userId, event) {
        this.notificationEvents.emit(this.getEventKey(userId), event);
    }
    handleClusterNotification(message) {
        if (message.channel !== this.channelName || !message.payload) {
            return;
        }
        let payload;
        try {
            payload = JSON.parse(message.payload);
        }
        catch (_a) {
            return;
        }
        if (!payload || payload.originInstanceId === this.instanceId) {
            return;
        }
        this.emitLocalNotificationEvent(payload.userId, {
            type: payload.type,
            notificationId: payload.notificationId,
            notification: payload.notification,
        });
    }
};
exports.NotificationService = NotificationService;
exports.NotificationService = NotificationService = NotificationService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_prisma_1.PrismaService))),
    __param(1, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_pagination_1.PaginationService))),
    __metadata("design:paramtypes", [api_prisma_1.PrismaService,
        api_pagination_1.PaginationService])
], NotificationService);
//# sourceMappingURL=notification.service.js.map