import * as Core from '../../../src/index.js';
import { Env, Logger, Queue } from '../../../src/index.js';
const RETRY_BASE_DELAY_MS = 1000;
const RETRY_MAX_DELAY_MS = 30000;
const getJobStateTracker = () => {
    try {
        return Core['JobStateTracker'];
    }
    catch {
        return undefined;
    }
};
const getJobHeartbeatStore = () => {
    try {
        return Core['JobHeartbeatStore'];
    }
    catch {
        return undefined;
    }
};
const getTimeoutManager = () => {
    try {
        return Core['TimeoutManager'];
    }
    catch {
        return undefined;
    }
};
const getEnvInt = (key, fallback) => {
    const getter = Env.getInt;
    if (typeof getter === 'function') {
        return getter(key, fallback);
    }
    const raw = Env[key];
    if (typeof raw === 'number' && Number.isFinite(raw))
        return Math.floor(raw);
    if (typeof raw === 'string' && raw.trim() !== '') {
        const parsed = Number(raw);
        if (Number.isFinite(parsed))
            return Math.floor(parsed);
    }
    return fallback;
};
const resolveQueueJobTimeoutMs = () => {
    const timeoutManager = getTimeoutManager();
    const tm = (timeoutManager ?? {});
    if (typeof tm.getQueueJobTimeoutMs === 'function') {
        return tm.getQueueJobTimeoutMs();
    }
    return Math.max(1000, getEnvInt('QUEUE_JOB_TIMEOUT', 60) * 1000);
};
const runWithTimeout = async (operation, timeoutMs, operationName) => {
    const timeoutManager = getTimeoutManager();
    const tm = (timeoutManager ?? {});
    if (typeof tm.withTimeout === 'function') {
        return tm.withTimeout(operation, timeoutMs, operationName);
    }
    return operation();
};
const isTimeoutError = (error) => {
    const timeoutManager = getTimeoutManager();
    const tm = (timeoutManager ?? {});
    if (typeof tm.isTimeoutError === 'function') {
        return tm.isTimeoutError(error);
    }
    if (error instanceof Error) {
        return error.message.toLowerCase().includes('timed out');
    }
    return false;
};
const normalizeAttempts = (value) => {
    if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
        return Math.floor(value);
    }
    return 0;
};
const getAttemptsFromMessage = (message) => {
    const payloadAttempts = typeof message.payload === 'object' && message.payload !== null
        ? normalizeAttempts(message.payload['attempts'])
        : 0;
    const messageAttempts = normalizeAttempts(message.attempts);
    return Math.max(payloadAttempts, messageAttempts);
};
const getRetryDelayMs = (nextAttempts) => {
    const exponentialDelay = Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.max(0, nextAttempts - 1), RETRY_MAX_DELAY_MS);
    const jitterMs = Math.floor(Math.random() * 250); // NOSONAR
    return exponentialDelay + jitterMs;
};
const buildBaseLogFields = (message, getLogFields) => {
    return {
        messageId: message.id,
        ...getLogFields(message.payload),
    };
};
const toBullMQPayload = (payload) => {
    if (typeof payload === 'object' && payload !== null) {
        return { ...payload };
    }
    return { payload };
};
const getWorkerInstanceId = () => {
    return typeof Env['WORKER_INSTANCE_ID'] === 'string'
        ? String(Env['WORKER_INSTANCE_ID'])
        : undefined;
};
const getTrackerApi = () => {
    return (getJobStateTracker() ?? {}) ?? {};
};
const getHeartbeatStoreApi = () => {
    return (getJobHeartbeatStore() ?? {}) ?? {};
};
const removeHeartbeatIfSupported = async (queueName, jobId) => {
    const heartbeatStore = getHeartbeatStoreApi();
    if (typeof heartbeatStore.remove === 'function') {
        await heartbeatStore.remove(queueName, jobId);
    }
};
const scheduleHeartbeatLoop = (trackerApi, queueName, jobId, workerInstanceId, heartbeatIntervalMs) => {
    return setInterval(() => {
        if (typeof trackerApi.heartbeat === 'function') {
            void trackerApi.heartbeat({
                queueName,
                jobId,
                workerInstanceId,
            });
        }
        const heartbeatStore = getHeartbeatStoreApi();
        if (typeof heartbeatStore.heartbeat === 'function') {
            void heartbeatStore.heartbeat({
                queueName,
                jobId,
                workerInstanceId,
                intervalMs: heartbeatIntervalMs,
            });
        }
    }, heartbeatIntervalMs);
};
const checkAndRequeueIfNotDue = async (options, queueName, driverName, message, baseLogFields) => {
    const payload = message.payload;
    const rawTimestamp = 'timestamp' in payload ? payload['timestamp'] : 0;
    const timestamp = typeof rawTimestamp === 'number' ? rawTimestamp : 0;
    if (timestamp <= Date.now())
        return false;
    Logger.info(`${options.kindLabel} not due yet, re-queueing`, {
        ...baseLogFields,
        dueAt: new Date(timestamp).toISOString(),
    });
    await Queue.enqueue(queueName, toBullMQPayload(message.payload), driverName);
    await Queue.ack(queueName, message.id, driverName);
    return true;
};
const onProcessSuccess = async (input) => {
    await Queue.ack(input.queueName, input.message.id, input.driverName);
    if (typeof input.trackerApi.completed === 'function') {
        await input.trackerApi.completed({
            queueName: input.queueName,
            jobId: input.message.id,
            processingTimeMs: Date.now() - input.startedAtMs,
        });
    }
    await removeHeartbeatIfSupported(input.queueName, input.message.id);
    Logger.info(`${input.options.kindLabel} processed successfully`, input.baseLogFields);
    return true;
};
const onProcessFailure = async (input) => {
    const attempts = getAttemptsFromMessage(input.message);
    const nextAttempts = attempts + 1;
    const isFinal = nextAttempts >= input.options.maxAttempts;
    let retryAt;
    Logger.error(`Failed to process ${input.options.kindLabel}`, {
        ...input.baseLogFields,
        error: input.error,
        attempts: nextAttempts,
    });
    if (isTimeoutError(input.error) && typeof input.trackerApi.timedOut === 'function') {
        await input.trackerApi.timedOut({
            queueName: input.queueName,
            jobId: input.message.id,
            reason: `Worker processing exceeded timeout for ${input.options.kindLabel}`,
            error: input.error,
        });
    }
    if (nextAttempts < input.options.maxAttempts) {
        const retryDelayMs = getRetryDelayMs(nextAttempts);
        retryAt = new Date(Date.now() + retryDelayMs).toISOString();
        const currentPayload = toBullMQPayload(input.message.payload);
        const payloadForRetry = {
            ...currentPayload,
            attempts: nextAttempts,
            timestamp: Date.now() + retryDelayMs,
        };
        await Queue.enqueue(input.queueName, payloadForRetry, input.driverName);
        Logger.info(`${input.options.kindLabel} re-queued for retry`, {
            ...input.baseLogFields,
            attempts: nextAttempts,
            retryDelayMs,
        });
    }
    await Queue.ack(input.queueName, input.message.id, input.driverName);
    await removeHeartbeatIfSupported(input.queueName, input.message.id);
    if (typeof input.trackerApi.failed === 'function') {
        await input.trackerApi.failed({
            queueName: input.queueName,
            jobId: input.message.id,
            attempts: nextAttempts,
            isFinal,
            retryAt,
            error: input.error,
        });
    }
    return true;
};
const startTrackingAndHeartbeat = async (input) => {
    const startedAtMs = Date.now();
    const timeoutMs = resolveQueueJobTimeoutMs();
    const heartbeatIntervalMs = Math.max(1000, getEnvInt('JOB_HEARTBEAT_INTERVAL_MS', 10000));
    const attempts = getAttemptsFromMessage(input.message);
    const workerInstanceId = getWorkerInstanceId();
    if (typeof input.trackerApi.started === 'function') {
        await input.trackerApi.started({
            queueName: input.queueName,
            jobId: input.message.id,
            attempts: attempts + 1,
            timeoutMs,
            workerName: input.options.kindLabel,
            workerInstanceId,
        });
    }
    const heartbeatStore = getHeartbeatStoreApi();
    if (typeof heartbeatStore.heartbeat === 'function') {
        await heartbeatStore.heartbeat({
            queueName: input.queueName,
            jobId: input.message.id,
            workerInstanceId,
            intervalMs: heartbeatIntervalMs,
        });
    }
    const heartbeatTimer = scheduleHeartbeatLoop(input.trackerApi, input.queueName, input.message.id, workerInstanceId, heartbeatIntervalMs);
    return { startedAtMs, heartbeatTimer };
};
const processQueueMessage = async (options, queueName, driverName) => {
    const message = await Queue.dequeue(queueName, driverName);
    if (!message)
        return false;
    const baseLogFields = buildBaseLogFields(message, options.getLogFields);
    const isRequeued = await checkAndRequeueIfNotDue(options, queueName, driverName, message, baseLogFields);
    if (isRequeued)
        return false;
    let heartbeatTimer;
    const trackerApi = getTrackerApi();
    const timeoutMs = resolveQueueJobTimeoutMs();
    let startedAtMs;
    try {
        const tracking = await startTrackingAndHeartbeat({
            options,
            trackerApi,
            queueName,
            message,
        });
        startedAtMs = tracking.startedAtMs;
        heartbeatTimer = tracking.heartbeatTimer;
        Logger.info(`Processing queued ${options.kindLabel}`, baseLogFields);
        await runWithTimeout(async () => {
            await options.handle(message.payload);
        }, timeoutMs, `${options.kindLabel}:${queueName}:${message.id}`);
        return onProcessSuccess({
            options,
            trackerApi,
            queueName,
            driverName,
            message,
            startedAtMs,
            baseLogFields,
        });
    }
    catch (error) {
        return onProcessFailure({
            options,
            trackerApi,
            queueName,
            driverName,
            message,
            baseLogFields,
            error,
        });
    }
    finally {
        if (heartbeatTimer !== undefined) {
            clearInterval(heartbeatTimer);
        }
    }
};
const createProcessOne = (options) => {
    return async (queueName = options.defaultQueueName, driverName) => {
        return processQueueMessage(options, queueName, driverName);
    };
};
const createProcessAll = (defaultQueueName, processOne) => {
    return async (queueName = defaultQueueName, driverName) => {
        let processed = 0;
        let hasMore = true;
        while (hasMore) {
            // eslint-disable-next-line no-await-in-loop
            hasMore = await processOne(queueName, driverName);
            if (hasMore)
                processed++;
        }
        return processed;
    };
};
const createRunOnce = (defaultQueueName, processOne) => {
    return async (opts = {}) => {
        const { queueName = defaultQueueName, driverName, maxItems, maxDurationMs = 30000, concurrency = 1, } = opts;
        const startTime = Date.now();
        let totalProcessed = 0;
        // Helper for single worker loop
        const runWorker = async () => {
            let workerProcessed = 0;
            while (true) {
                if (maxDurationMs > 0 && Date.now() - startTime > maxDurationMs) {
                    break;
                }
                if (maxItems !== undefined && totalProcessed >= maxItems) {
                    break;
                }
                // eslint-disable-next-line no-await-in-loop
                const didProcess = await processOne(queueName, driverName);
                if (!didProcess)
                    break;
                workerProcessed++;
                totalProcessed++; // Shared counter (approximation in parallel)
            }
            return workerProcessed;
        };
        // Run workers in parallel
        await Promise.all(Array.from({ length: Math.max(1, concurrency) }).map(() => runWorker()));
        return totalProcessed;
    };
};
const createStartWorker = (kindLabel, defaultQueueName, processOne) => {
    return async (opts = {}) => {
        const { queueName = defaultQueueName, driverName, signal, maxDurationMs = 300000, concurrency = 1, } = opts;
        Logger.info(`Starting ${kindLabel} worker (drain-until-empty)`, { queueName, concurrency });
        const startTime = Date.now();
        let totalProcessed = 0;
        const runWorker = async () => {
            while (signal?.aborted !== true) {
                if (maxDurationMs > 0 && Date.now() - startTime > maxDurationMs) {
                    Logger.warn(`${kindLabel} worker timeout reached`, { queueName, totalProcessed });
                    break;
                }
                // eslint-disable-next-line no-await-in-loop
                const didProcess = await processOne(queueName, driverName);
                if (!didProcess)
                    break;
                totalProcessed++;
            }
        };
        // Run workers in parallel
        await Promise.all(Array.from({ length: Math.max(1, concurrency) }).map(() => runWorker()));
        Logger.info(`${kindLabel} worker finished (queue drained)`, { queueName, totalProcessed });
        return totalProcessed;
    };
};
export function createQueueWorker(options) {
    const processOne = createProcessOne(options);
    const processAll = createProcessAll(options.defaultQueueName, processOne);
    const runOnce = createRunOnce(options.defaultQueueName, processOne);
    const startWorker = createStartWorker(options.kindLabel, options.defaultQueueName, processOne);
    const queueWorkerMeta = Object.freeze({
        kindLabel: options.kindLabel,
        defaultQueueName: options.defaultQueueName,
        maxAttempts: options.maxAttempts,
    });
    return Object.freeze({
        processOne,
        processAll,
        runOnce,
        startWorker,
        __zintrustQueueWorkerMeta: queueWorkerMeta,
    });
}
