import { fsImportAll } from '@adonisjs/core/helpers';
import string from '@adonisjs/core/helpers/string';
import app from '@adonisjs/core/services/app';
import { PgBossCheck } from '@akago/adonisjs-pg-boss/healthcheck';
import { Sentry } from '@rlanz/sentry';
import _ from 'lodash';
import path from 'node:path';
import { SENTRY_SPAN_NAME_IGNORE } from '../../config/sentry.js';
import { authContext } from '../auth/auth_context/provider.js';
export class AKEventService {
    pgBoss;
    userService;
    logger;
    channels;
    static synchronous = ['console', 'test', 'repl'].includes(app.getEnvironment());
    constructor(pgBoss, userService, logger, channels) {
        this.pgBoss = pgBoss;
        this.userService = userService;
        this.logger = logger;
        this.channels = channels;
    }
    async init() {
        if (!AKEventService.synchronous) {
            await this.#initEventQueues();
        }
        await this.fetchEmailPreviewEvents();
    }
    async #initEventQueues() {
        this.pgBoss.on('error', (e) => {
            this.logger.error('PgBoss error', e);
            Sentry.captureException(e);
        });
        await this.pgBoss.start();
        for (const eventClass of await this.fetchEventClasses()) {
            const queueName = eventClass.jobQueueName;
            await this.pgBoss.createQueue(queueName);
            this.logger.debug(`Queue created for event ${eventClass.name} (queue name: ${queueName})`);
            const span = Sentry.startInactiveSpan({ name: SENTRY_SPAN_NAME_IGNORE });
            await this.pgBoss.work(queueName, async (jobs) => {
                for (const job of jobs) {
                    await authContext.runWithSnapshot(job.data.auth, async () => {
                        await this.traceAndProcess(job.id, eventClass, job.data.job);
                    });
                }
            });
            span.end();
        }
    }
    async clearQueues() {
        if (!AKEventService.synchronous) {
            await this.pgBoss.deleteAllJobs();
        }
    }
    static makeHealthChecks() {
        return this.synchronous ? [] : [new PgBossCheck('Job queue')];
    }
    async publish(event) {
        const eventClass = event.constructor;
        const queueName = eventClass.jobQueueName;
        const eventData = event.toEventData();
        if (!AKEventService.synchronous) {
            if (event.jobDebounceMilliseconds && event.jobSingletonKey) {
                const jobsToDebounce = await this.pgBoss.findJobs(queueName, {
                    key: event.jobSingletonKey,
                });
                const jobsToDebounceIds = jobsToDebounce.map((j) => j.id);
                if (jobsToDebounceIds.length > 0) {
                    this.logger.debug(`Debouncing ${jobsToDebounceIds.length} jobs in queue ${queueName} [${jobsToDebounceIds.join(', ')}]`);
                    await this.pgBoss.cancel(queueName, jobsToDebounceIds);
                }
            }
            const jobId = await this.pgBoss.send({
                name: queueName,
                data: { job: eventData, auth: authContext.snapshot() },
                options: {
                    retryLimit: 1,
                    startAfter: Math.round((event.jobDebounceMilliseconds ?? 0) / 1000),
                    expireInSeconds: 60,
                    singletonKey: event.jobSingletonKey,
                },
            });
            await this.#traceEventPublished(jobId, queueName);
            if (jobId === null)
                throw new Error('Could not enqueue event ${queueName}');
            else
                this.logger.debug(`Queued event ${jobId} to queue ${queueName}`);
        }
        else {
            const jobId = string.random(20);
            await this.#traceEventPublished(jobId, queueName);
            await this.traceAndProcess(jobId, eventClass, eventData);
        }
    }
    async traceAndProcess(eventId, eventClass, eventData) {
        this.logger.debug(`Processing event ${eventId} of type ${eventClass.name}`);
        await this.#traceEventConsumed(eventId, eventClass, eventData, async (span) => {
            const event = await eventClass.fromEventData(eventData, this.userService);
            try {
                await this.process(event);
                this.logger.debug(`Sucessfully processed event ${eventId} of type ${eventClass.name}`);
                span.setStatus({ code: 1, message: 'ok' });
            }
            catch (error) {
                this.logger.error(`Error while processing event ${eventId} of type ${eventClass.name}: ${error}`);
                Sentry.captureException(error);
                span.recordException(error);
                span.setStatus({ code: 2, message: error.message });
                throw error;
            }
        });
    }
    async process(event) {
        const fakableChannelsNames = ['email', 'pushNotification'];
        for (const channelName of event.shouldSendViaChannels())
            if (channelName in this.channels) {
                const channel = this.channels[channelName];
                if (channel.shouldProcess === undefined || channel.shouldProcess(event))
                    if (this.#isFake && fakableChannelsNames.includes(channelName)) {
                        this.#fakeEvents[channelName].push(event);
                    }
                    else {
                        await channel.process(event);
                    }
            }
    }
    async #traceEventPublished(jobId, queueName) {
        if (jobId != null)
            await Sentry.startSpan({
                name: 'queue_producer',
                op: 'queue.publish',
                attributes: {
                    'messaging.message.id': jobId,
                    'messaging.destination.name': queueName,
                    'messaging.message.body.size': 1,
                },
            }, () => null);
    }
    #traceEventConsumed(eventId, eventClass, eventData, block) {
        return Sentry.startSpan({
            name: `Event : ${eventClass.name}`,
            op: 'queue.process',
            attributes: {
                'messaging.message.id': eventId,
                'messaging.destination.name': eventClass.jobQueueName,
                'messaging.message.body.size': 1,
                'messaging.message.retry.count': 0,
                ..._.mapKeys(eventData, (_v, k) => 'job_data:' + k),
            },
        }, block);
    }
    #isFake = false;
    #fakeEvents = {
        pushNotification: [],
        email: [],
    };
    static async fake() {
        const service = await app.container.make('akago.event.service');
        service.#isFake = true;
        return {
            mails: service.#fakeEvents.email,
            pushNotifications: service.#fakeEvents.pushNotification,
            restore: () => {
                service.#isFake = false;
                for (const arrayToEmpty of Object.values(service.#fakeEvents))
                    while (arrayToEmpty.length)
                        arrayToEmpty.pop();
            },
        };
    }
    emailPreviewEvents;
    async fetchEmailPreviewEvents() {
        this.emailPreviewEvents = (await this.fetchEventClasses())
            .map((eventClass) => eventClass.preview())
            .filter((e) => e !== null && e.shouldSendViaChannels().includes('email'));
    }
    async fetchEventClasses() {
        const libEvents = new URL(`./events`, import.meta.url);
        const appEvents = app.makePath('app/events/');
        return (await Promise.all([libEvents, appEvents].map((p) => fsImportAll(p, {
            filter: (p) => path.basename(p).endsWith('_event.ts'),
        }))))
            .map(Object.values)
            .flat();
    }
}
//# sourceMappingURL=event_service.js.map