import { AdaptiveConcurrencyController, isAdaptiveAgentConcurrencyEnabled, resolveAgentConcurrencyCap, } from '../native/adaptive-concurrency.js';
const DEFAULT_QUEUE_MAX = 64;
const DEFAULT_WAIT_MS = 30_000;
const DEFAULT_BACKPRESSURE_RETRY_MS = 2_000;
function envInt(name, fallback) {
    const parsed = Number.parseInt(String(process.env[name] ?? ''), 10);
    if (!Number.isFinite(parsed) || parsed <= 0)
        return fallback;
    return Math.floor(parsed);
}
function abortError() {
    const err = new Error('Admission aborted');
    err.name = 'AbortError';
    return err;
}
/** Thrown when a request can't be admitted (queue full or wait exceeded). */
export class AdmissionBackpressureError extends Error {
    retryAfterMs;
    constructor(retryAfterMs, message) {
        super(message);
        this.name = 'AdmissionBackpressureError';
        this.retryAfterMs = retryAfterMs;
    }
}
export class AdmissionGate {
    states = new Map();
    limitFor;
    queueMax;
    waitMs;
    backpressureRetryMs;
    constructor(opts = {}) {
        this.limitFor = opts.limitFor ?? ((_key, cap) => cap);
        this.queueMax = opts.queueMax ?? (() => envInt('OWLCODA_DAEMON_ADMIT_QUEUE_MAX', DEFAULT_QUEUE_MAX));
        this.waitMs = opts.waitMs ?? (() => envInt('OWLCODA_DAEMON_ADMIT_WAIT_MS', DEFAULT_WAIT_MS));
        this.backpressureRetryMs = opts.backpressureRetryMs ?? (() => DEFAULT_BACKPRESSURE_RETRY_MS);
    }
    /**
     * Acquire a slot for `key`. Resolves with a release() callback when admitted;
     * rejects with AdmissionBackpressureError (queue full / wait exceeded) or an
     * AbortError (signal aborted, e.g. client disconnect).
     */
    async admit(key, cap, signal) {
        if (signal?.aborted)
            throw abortError();
        const st = this.stateFor(key, cap);
        if (st.inFlight < this.limitFor(key, cap)) {
            st.inFlight += 1;
            st.admitted += 1;
            return this.makeRelease(key);
        }
        if (st.queue.length >= this.queueMax()) {
            st.rejected += 1;
            throw new AdmissionBackpressureError(this.backpressureRetryMs(), 'admission queue full');
        }
        await new Promise((resolve, reject) => {
            const waiter = {
                resolve,
                reject,
                timer: setTimeout(() => {
                    this.removeWaiter(st, waiter);
                    st.rejected += 1;
                    reject(new AdmissionBackpressureError(this.backpressureRetryMs(), 'admission wait timeout'));
                }, this.waitMs()),
                cleanup: () => { },
            };
            if (signal) {
                const onAbort = () => {
                    this.removeWaiter(st, waiter);
                    reject(abortError());
                };
                signal.addEventListener('abort', onAbort, { once: true });
                waiter.cleanup = () => signal.removeEventListener('abort', onAbort);
            }
            st.queue.push(waiter);
        });
        // pump() already incremented inFlight before resolving this waiter.
        return this.makeRelease(key);
    }
    makeRelease(key) {
        let released = false;
        return () => {
            if (released)
                return;
            released = true;
            const st = this.states.get(key);
            if (!st)
                return;
            st.inFlight = Math.max(0, st.inFlight - 1);
            this.pump(key);
        };
    }
    pump(key) {
        const st = this.states.get(key);
        if (!st)
            return;
        while (st.queue.length > 0 && st.inFlight < this.limitFor(key, st.cap)) {
            const waiter = st.queue.shift();
            clearTimeout(waiter.timer);
            waiter.cleanup();
            st.inFlight += 1;
            st.admitted += 1;
            waiter.resolve();
        }
    }
    removeWaiter(st, waiter) {
        const idx = st.queue.indexOf(waiter);
        if (idx !== -1)
            st.queue.splice(idx, 1);
        clearTimeout(waiter.timer);
        waiter.cleanup();
    }
    stateFor(key, cap) {
        const existing = this.states.get(key);
        if (existing) {
            existing.cap = cap;
            return existing;
        }
        const created = { inFlight: 0, queue: [], cap, admitted: 0, rejected: 0 };
        this.states.set(key, created);
        return created;
    }
    /** Per-key observability snapshot (current state + lifetime counters). */
    snapshot() {
        const out = [];
        for (const [key, st] of this.states) {
            out.push({
                key,
                inFlight: st.inFlight,
                queued: st.queue.length,
                limit: this.limitFor(key, st.cap),
                admitted: st.admitted,
                rejected: st.rejected,
            });
        }
        return out;
    }
    /** Re-run admission for a key after its limit may have increased. */
    notifyLimitChanged(key) {
        this.pump(key);
    }
    reset() {
        for (const st of this.states.values()) {
            for (const w of st.queue) {
                clearTimeout(w.timer);
                w.cleanup();
            }
        }
        this.states.clear();
    }
    inFlightForTesting(key) {
        return this.states.get(key)?.inFlight ?? 0;
    }
    queueDepthForTesting(key) {
        return this.states.get(key)?.queue.length ?? 0;
    }
}
// Daemon-side AIMD: the admission limit per key adapts (slow-start from 1, +1
// after sustained success, halve on a rate-limit signal) up to the cap. This is
// the cross-process adaptive authority; clients delegate to it (their semaphore
// is a static ceiling when the flag is on).
const controller = new AdaptiveConcurrencyController();
const gate = new AdmissionGate({ limitFor: (key, cap) => controller.currentLimit(key, cap) });
/** Stable per-upstream key: the real backend endpoint + model, not the proxy base. */
export function admissionKeyFromUpstream(endpointUrl, backendModel) {
    const base = (endpointUrl ?? '').trim() || 'unknown';
    const model = (backendModel ?? '').trim();
    return `${base}::${model}`;
}
/**
 * Acquire a cross-process admission slot. No-op (zero overhead) when the
 * adaptive-concurrency flag is off.
 */
export async function admitRequest(key, signal) {
    if (!isAdaptiveAgentConcurrencyEnabled())
        return () => { };
    return gate.admit(key, resolveAgentConcurrencyCap(), signal);
}
// Lifetime AIMD-signal counters per key (observability for dogfood).
const successCounts = new Map();
const rateLimitCounts = new Map();
/** Record a successful upstream request: AIMD may raise the limit → admit waiters. */
export function recordAdmissionSuccess(key) {
    if (!isAdaptiveAgentConcurrencyEnabled())
        return;
    controller.recordSuccess(key, resolveAgentConcurrencyCap());
    successCounts.set(key, (successCounts.get(key) ?? 0) + 1);
    gate.notifyLimitChanged(key);
}
/** Record a rate-limit signal: AIMD halves the limit (no pump — fewer slots). */
export function recordAdmissionRateLimit(key) {
    if (!isAdaptiveAgentConcurrencyEnabled())
        return;
    controller.recordRateLimit(key, resolveAgentConcurrencyCap());
    rateLimitCounts.set(key, (rateLimitCounts.get(key) ?? 0) + 1);
}
/** Observability snapshot of the admission gate (config + per-key live state + counters). */
export function admissionStatus() {
    const cap = resolveAgentConcurrencyCap();
    const byKey = new Map();
    for (const s of gate.snapshot()) {
        byKey.set(s.key, { ...s, success: successCounts.get(s.key) ?? 0, rateLimit: rateLimitCounts.get(s.key) ?? 0 });
    }
    // Union in keys that have only signal counters (no live gate state yet).
    for (const key of new Set([...successCounts.keys(), ...rateLimitCounts.keys()])) {
        if (byKey.has(key))
            continue;
        byKey.set(key, {
            key,
            limit: controller.currentLimit(key, cap),
            inFlight: 0,
            queued: 0,
            admitted: 0,
            rejected: 0,
            success: successCounts.get(key) ?? 0,
            rateLimit: rateLimitCounts.get(key) ?? 0,
        });
    }
    return {
        enabled: isAdaptiveAgentConcurrencyEnabled(),
        cap,
        queueMax: envInt('OWLCODA_DAEMON_ADMIT_QUEUE_MAX', DEFAULT_QUEUE_MAX),
        waitMs: envInt('OWLCODA_DAEMON_ADMIT_WAIT_MS', DEFAULT_WAIT_MS),
        keys: [...byKey.values()],
    };
}
/** GET /v1/admission — cross-process admission gate observability. */
export function handleAdmissionStatus(_req, res) {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(admissionStatus()));
}
export function __resetAdmissionForTesting() {
    gate.reset();
    controller.reset();
    successCounts.clear();
    rateLimitCounts.clear();
}
export function __admissionLimitForTesting(key, cap = resolveAgentConcurrencyCap()) {
    return controller.currentLimit(key, cap);
}
export function __admissionGateForTesting() {
    return gate;
}
//# sourceMappingURL=admission.js.map