/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { SCHEDULER_PRIORITIES } from '../types/runtime-scheduler.js';
const DEFAULT_CONFIG = {
    maxConcurrency: 4,
    circuitBreaker: {
        failureThreshold: 5,
        cooldownMs: 30_000,
        halfOpenMaxAttempts: 1,
    },
    aging: {
        intervalMs: 10_000,
        maxPromotions: 2,
    },
};
let config = { ...DEFAULT_CONFIG };
let queues = createEmptyQueues();
let activeCount = 0;
let circuitState = 'closed';
let consecutiveFailures = 0;
let lastFailureAt = null;
let circuitOpenedAt = null;
let halfOpenAttempts = 0;
let agingTimer = null;
function createEmptyQueues() {
    return { critical: [], high: [], normal: [], low: [] };
}
export function configureScheduler(partial) {
    config = {
        ...config,
        ...partial,
        circuitBreaker: { ...config.circuitBreaker, ...partial.circuitBreaker },
        aging: { ...config.aging, ...partial.aging },
    };
    if (config.maxConcurrency < 1)
        config.maxConcurrency = 1;
}
export function resetScheduler() {
    queues = createEmptyQueues();
    activeCount = 0;
    circuitState = 'closed';
    consecutiveFailures = 0;
    lastFailureAt = null;
    circuitOpenedAt = null;
    halfOpenAttempts = 0;
    config = { ...DEFAULT_CONFIG };
    stopAging();
}
export function getSchedulerStatus() {
    return {
        queued: {
            critical: queues.critical.length,
            high: queues.high.length,
            normal: queues.normal.length,
            low: queues.low.length,
        },
        active: activeCount,
        maxConcurrency: config.maxConcurrency,
        circuit: {
            state: circuitState,
            failures: consecutiveFailures,
            lastFailureAt,
            openedAt: circuitOpenedAt,
        },
    };
}
export function enqueue(id, priority, execute) {
    const task = {
        id,
        priority,
        enqueuedAt: Date.now(),
        promotions: 0,
        execute,
    };
    queues[priority].push(task);
    drain();
}
export async function submit(id, priority, execute) {
    return new Promise((resolve, reject) => {
        const wrapped = async () => {
            try {
                const result = await execute();
                resolve(result);
                return result;
            }
            catch (err) {
                reject(err);
                throw err;
            }
        };
        enqueue(id, priority, wrapped);
    });
}
function evaluateCircuit() {
    if (circuitState === 'closed')
        return 'closed';
    if (circuitState === 'open') {
        if (circuitOpenedAt !== null && Date.now() - circuitOpenedAt >= config.circuitBreaker.cooldownMs) {
            circuitState = 'half-open';
            halfOpenAttempts = 0;
            return 'half-open';
        }
        return 'open';
    }
    return 'half-open';
}
function recordSuccess() {
    consecutiveFailures = 0;
    lastFailureAt = null;
    if (circuitState === 'half-open') {
        circuitState = 'closed';
        circuitOpenedAt = null;
    }
}
function recordFailure() {
    consecutiveFailures++;
    lastFailureAt = Date.now();
    if (circuitState === 'half-open') {
        halfOpenAttempts++;
        if (halfOpenAttempts >= config.circuitBreaker.halfOpenMaxAttempts) {
            circuitState = 'open';
            circuitOpenedAt = Date.now();
        }
        return;
    }
    if (consecutiveFailures >= config.circuitBreaker.failureThreshold) {
        circuitState = 'open';
        circuitOpenedAt = Date.now();
    }
}
function dequeue() {
    for (const priority of SCHEDULER_PRIORITIES) {
        if (queues[priority].length > 0) {
            return queues[priority].shift();
        }
    }
    return undefined;
}
function drain() {
    const currentState = evaluateCircuit();
    if (currentState === 'open')
        return;
    while (activeCount < config.maxConcurrency) {
        if (currentState === 'half-open' && activeCount > 0)
            break;
        const task = dequeue();
        if (!task)
            break;
        activeCount++;
        task
            .execute()
            .then(() => {
            recordSuccess();
        })
            .catch(() => {
            recordFailure();
        })
            .finally(() => {
            activeCount--;
            drain();
        });
    }
}
export function promoteAged() {
    let promoted = 0;
    const priorityOrder = SCHEDULER_PRIORITIES;
    const alreadyPromoted = new Set();
    for (let i = priorityOrder.length - 1; i > 0; i--) {
        const currentPriority = priorityOrder[i];
        const higherPriority = priorityOrder[i - 1];
        promoted += promoteQueueLevel(queues[currentPriority], higherPriority, alreadyPromoted);
    }
    if (promoted > 0)
        drain();
    return promoted;
}
function promoteQueueLevel(queue, higherPriority, alreadyPromoted) {
    const toPromote = findPromotableIndices(queue, alreadyPromoted);
    let promoted = 0;
    for (let k = toPromote.length - 1; k >= 0; k--) {
        const idx = toPromote[k];
        const [task] = queue.splice(idx, 1);
        task.promotions++;
        task.priority = higherPriority;
        queues[higherPriority].push(task);
        alreadyPromoted.add(task.id);
        promoted++;
    }
    return promoted;
}
function findPromotableIndices(queue, alreadyPromoted) {
    const indices = [];
    const now = Date.now();
    for (let j = 0; j < queue.length; j++) {
        const task = queue[j];
        if (alreadyPromoted.has(task.id))
            continue;
        if (task.promotions >= config.aging.maxPromotions)
            continue;
        if (now - task.enqueuedAt >= config.aging.intervalMs) {
            indices.push(j);
        }
    }
    return indices;
}
export function startAging() {
    if (agingTimer)
        return;
    agingTimer = setInterval(() => {
        promoteAged();
    }, config.aging.intervalMs);
}
export function stopAging() {
    if (agingTimer) {
        clearInterval(agingTimer);
        agingTimer = null;
    }
}
//# sourceMappingURL=runtime-scheduler.js.map