export class RouteLoadBalancer {
    policy;
    states = new Map();
    constructor(policy) {
        this.policy = policy ?? { strategy: 'round-robin' };
    }
    updatePolicy(policy) {
        if (policy) {
            this.policy = policy;
        }
    }
    getPolicy() {
        return this.policy;
    }
    select(options, strategyOverride) {
        const available = options.candidates.filter((candidate) => options.availabilityCheck(candidate));
        if (available.length === 0) {
            return null;
        }
        const strategy = strategyOverride ?? this.policy.strategy;
        switch (strategy) {
            case 'sticky':
                return this.selectSticky(options.routeName, available, options.stickyKey, options.weights ?? this.policy.weights);
            case 'weighted':
                return this.selectWeighted(options.routeName, available, options.weights ?? this.policy.weights);
            default:
                if (options.weights) {
                    const distinct = new Set(available.map((candidate) => normalizeWeight(options.weights?.[candidate])));
                    if (distinct.size > 1) {
                        return this.selectWeighted(options.routeName, available, options.weights);
                    }
                }
                return this.selectRoundRobin(options.routeName, available);
        }
    }
    selectGrouped(options, strategyOverride) {
        const groupIds = Array.from(options.groups.keys()).filter((groupId) => {
            const members = options.groups.get(groupId) ?? [];
            return members.some((candidate) => options.availabilityCheck(candidate));
        });
        if (groupIds.length === 0) {
            return null;
        }
        const normalizedWeights = normalizeGroupWeights(groupIds, options.weights);
        const strategy = strategyOverride ?? this.policy.strategy;
        const groupRoute = `${options.routeName}:group`;
        let selectedGroup = null;
        switch (strategy) {
            case 'sticky':
                selectedGroup = this.selectSticky(groupRoute, groupIds, options.stickyKey, normalizedWeights);
                break;
            case 'weighted':
                selectedGroup = this.selectWeighted(groupRoute, groupIds, normalizedWeights);
                break;
            default:
                selectedGroup = this.selectRoundRobin(groupRoute, groupIds);
                break;
        }
        if (!selectedGroup) {
            return null;
        }
        const groupCandidates = (options.groups.get(selectedGroup) ?? []).filter((candidate) => options.availabilityCheck(candidate));
        if (groupCandidates.length === 0) {
            return null;
        }
        return this.selectRoundRobin(`${groupRoute}:${selectedGroup}`, groupCandidates);
    }
    selectRoundRobin(routeName, candidates) {
        const state = this.getState(routeName);
        const choice = candidates[state.pointer % candidates.length];
        state.pointer = (state.pointer + 1) % candidates.length;
        return choice;
    }
    selectWeighted(routeName, candidates, weights) {
        // Deterministic smooth weighted round-robin (no randomness) so routing behavior is testable and stable.
        // Each candidate with a positive weight is guaranteed to be selected eventually.
        const state = this.getState(routeName);
        const current = state.weighted.currentWeights;
        const candidateSet = new Set(candidates);
        for (const existing of Array.from(current.keys())) {
            if (!candidateSet.has(existing)) {
                current.delete(existing);
            }
        }
        for (const key of candidates) {
            if (!current.has(key)) {
                current.set(key, 0);
            }
        }
        const candidateWeights = candidates.map((candidate) => normalizeWeight(weights?.[candidate]));
        const totalWeight = candidateWeights.reduce((sum, w) => sum + w, 0);
        if (!Number.isFinite(totalWeight) || totalWeight <= 0) {
            return this.selectRoundRobin(routeName, candidates);
        }
        let bestIndex = 0;
        let bestScore = Number.NEGATIVE_INFINITY;
        for (let i = 0; i < candidates.length; i += 1) {
            const key = candidates[i];
            const w = candidateWeights[i];
            const next = (current.get(key) ?? 0) + w;
            current.set(key, next);
            if (next > bestScore) {
                bestScore = next;
                bestIndex = i;
            }
        }
        const selectedKey = candidates[bestIndex];
        current.set(selectedKey, (current.get(selectedKey) ?? 0) - totalWeight);
        return selectedKey;
    }
    selectSticky(routeName, candidates, stickyKey, weights) {
        if (!stickyKey) {
            return this.selectRoundRobin(routeName, candidates);
        }
        const state = this.getState(routeName);
        const pinned = state.stickyMap.get(stickyKey);
        if (pinned && candidates.includes(pinned)) {
            return pinned;
        }
        const choice = weights && Object.keys(weights).length > 0
            ? this.selectWeighted(`${routeName}:sticky`, candidates, weights)
            : this.selectRoundRobin(routeName, candidates);
        state.stickyMap.set(stickyKey, choice);
        return choice;
    }
    getState(routeName) {
        if (!this.states.has(routeName)) {
            this.states.set(routeName, { pointer: 0, stickyMap: new Map(), weighted: { currentWeights: new Map() } });
        }
        return this.states.get(routeName);
    }
}
function normalizeWeight(value) {
    if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
        return value;
    }
    return 1;
}
function normalizeGroupWeights(groupIds, weights) {
    if (!weights || groupIds.length === 0) {
        return undefined;
    }
    const normalized = {};
    let total = 0;
    for (const id of groupIds) {
        const raw = weights[id];
        const value = typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? raw : 0;
        if (value > 0) {
            normalized[id] = value;
            total += value;
        }
    }
    if (!Number.isFinite(total) || total <= 0) {
        return undefined;
    }
    for (const id of Object.keys(normalized)) {
        normalized[id] = normalized[id] / total;
    }
    return normalized;
}
