import { Env } from '../../../config/env.js';
import { Logger } from '../../../config/logger.js';
import { ErrorFactory } from '../../../exceptions/ZintrustError.js';
import { useDatabase } from '../../../orm/Database.js';
import { QueryBuilder } from '../../../orm/QueryBuilder.js';
export const DatabaseQueue = (() => {
    const connections = new Map();
    const getConnection = (connectionName) => {
        const name = connectionName ?? Env.get('DB_CONNECTION', 'default');
        if (!connections.has(name)) {
            connections.set(name, useDatabase(undefined, name));
        }
        const connection = connections.get(name);
        if (!connection) {
            throw ErrorFactory.createConfigError(`Database connection '${name}' not found`);
        }
        return connection;
    };
    const getTableName = () => Env.get('QUEUE_DB_TABLE', 'queue_jobs');
    return {
        async enqueue(queue, payload, connectionName) {
            const db = getConnection(connectionName);
            const tableName = getTableName();
            const result = await QueryBuilder.create(tableName, db).insert({
                queue,
                payload: JSON.stringify(payload),
                attempts: 0,
                max_attempts: Env.getInt('QUEUE_DB_RETRY_ATTEMPTS', 3),
                created_at: new Date(),
                available_at: new Date(),
            });
            const jobId = result.id;
            Logger.info(`[DatabaseQueue] Job enqueued: ${jobId} to queue: ${queue}`);
            return jobId;
        },
        async dequeue(queue, connectionName) {
            const db = getConnection(connectionName);
            const tableName = getTableName();
            const visibilityTimeout = Env.getInt('QUEUE_DB_VISIBILITY_TIMEOUT', 30);
            const timeoutDate = new Date(Date.now() - visibilityTimeout * 1000);
            // Try to reserve a job using atomic update
            const result = await db.transaction(async (trx) => {
                const job = await QueryBuilder.create(tableName, trx)
                    .select('id', 'payload', 'attempts', 'max_attempts')
                    .where('queue', '=', queue)
                    .where('available_at', '<=', new Date())
                    .where('reserved_at', '=', null)
                    .orWhere('reserved_at', '<=', timeoutDate)
                    .where('failed_at', '=', null)
                    .orderBy('available_at', 'ASC')
                    .limit(1)
                    .first();
                if (!job)
                    return null;
                // Check if job has exceeded max attempts
                if (job.attempts >= job.max_attempts) {
                    // Move to dead letter queue
                    await QueryBuilder.create('queue_jobs_failed', trx).insert({
                        original_id: job.id,
                        queue,
                        payload: job.payload,
                        attempts: job.attempts,
                        failed_at: new Date(),
                        error_message: 'Max attempts exceeded',
                    });
                    // Remove from active queue
                    await QueryBuilder.create(tableName, trx).where('id', '=', job.id).delete();
                    Logger.warn(`[DatabaseQueue] Job ${job.id} exceeded max attempts (${job.max_attempts}), moved to dead letter queue`);
                    return null;
                }
                // Calculate exponential backoff delay
                const backoffDelay = Math.min(Math.pow(2, job.attempts) * 1000, 30000); // Max 30 seconds
                const nextAvailableAt = new Date(Date.now() + backoffDelay);
                // Reserve the job and set next available time
                await QueryBuilder.create(tableName, trx)
                    .where('id', '=', job.id)
                    .update({
                    reserved_at: new Date(),
                    attempts: job.attempts + 1,
                    available_at: nextAvailableAt,
                });
                Logger.debug(`[DatabaseQueue] Job ${job.id} reserved, attempt ${job.attempts + 1}/${job.max_attempts}, next available at ${nextAvailableAt.toISOString()}`);
                return job;
            });
            if (!result)
                return undefined;
            return {
                id: result.id,
                payload: JSON.parse(result.payload),
                attempts: result.attempts,
            };
        },
        async ack(queue, id, connectionName) {
            const db = getConnection(connectionName);
            const tableName = getTableName();
            await QueryBuilder.create(tableName, db)
                .where('id', '=', id)
                .where('queue', '=', queue)
                .delete();
            Logger.debug(`[DatabaseQueue] Job acknowledged: ${id} from queue: ${queue}`);
        },
        async length(queue, connectionName) {
            const db = getConnection(connectionName);
            const tableName = getTableName();
            const result = await QueryBuilder.create(tableName, db)
                .select('id')
                .where('queue', '=', queue)
                .where('available_at', '<=', new Date())
                .where('reserved_at', '=', null)
                .orWhere('reserved_at', '<=', new Date(Date.now() - Env.getInt('QUEUE_DB_VISIBILITY_TIMEOUT', 30) * 1000))
                .where('failed_at', '=', null)
                .get();
            return result?.length ?? 0;
        },
        async drain(queue, connectionName) {
            const db = getConnection(connectionName);
            const tableName = getTableName();
            await QueryBuilder.create(tableName, db).where('queue', '=', queue).delete();
            Logger.info(`[DatabaseQueue] Queue drained: ${queue}`);
        },
    };
})();
export default DatabaseQueue;
