import { ProbeNameCollisionError } from './errors.js';
function compareVeto(a, b) {
    if (a.priority !== b.priority)
        return a.priority - b.priority;
    return a.id - b.id;
}
function insertSortedVeto(list, entry) {
    let lo = 0;
    let hi = list.length;
    while (lo < hi) {
        const mid = (lo + hi) >>> 1;
        if (compareVeto(list[mid], entry) <= 0)
            lo = mid + 1;
        else
            hi = mid;
    }
    list.splice(lo, 0, entry);
}
function removeVeto(list, entry) {
    const idx = list.indexOf(entry);
    if (idx >= 0)
        list.splice(idx, 1);
}
function normalizeDecision(decision) {
    if (decision === 'allow')
        return { allow: true };
    if (decision === 'deny')
        return { allow: false };
    return { allow: false, reason: decision.reason };
}
function compareEntries(a, b) {
    if (a.priority !== b.priority)
        return a.priority - b.priority;
    return a.id - b.id;
}
function insertSorted(list, entry) {
    let lo = 0;
    let hi = list.length;
    while (lo < hi) {
        const mid = (lo + hi) >>> 1;
        if (compareEntries(list[mid], entry) <= 0)
            lo = mid + 1;
        else
            hi = mid;
    }
    list.splice(lo, 0, entry);
}
function removeEntry(list, entry) {
    const idx = list.indexOf(entry);
    if (idx >= 0)
        list.splice(idx, 1);
}
export class ProbeRegistry {
    typedByKind = new Map();
    vetoByKind = new Map();
    catchAll = [];
    byName = new Map();
    nextId = 1;
    log;
    setLogger(log) {
        this.log = log.child({ component: 'ProbeRegistry' });
    }
    on(kind, handler, opts = {}) {
        const kinds = Array.isArray(kind) ? kind : [kind];
        const entry = this.makeEntry(handler, opts);
        for (const k of kinds) {
            let bucket = this.typedByKind.get(k);
            if (!bucket) {
                bucket = [];
                this.typedByKind.set(k, bucket);
            }
            insertSorted(bucket, entry);
        }
        return () => {
            for (const k of kinds) {
                const bucket = this.typedByKind.get(k);
                if (bucket)
                    removeEntry(bucket, entry);
            }
            if (entry.name)
                this.byName.delete(entry.name);
        };
    }
    onAny(handler, opts = {}) {
        const entry = this.makeEntry(handler, opts);
        insertSorted(this.catchAll, entry);
        return () => {
            removeEntry(this.catchAll, entry);
            if (entry.name)
                this.byName.delete(entry.name);
        };
    }
    dispatch(event, ctx, betweenTier) {
        const frozen = Object.isFrozen(event) ? event : Object.freeze(event);
        this.runTier(this.typedByKind.get(frozen.type) ?? [], frozen, ctx);
        if (betweenTier) {
            try {
                betweenTier();
            }
            catch (error) {
                this.logThrow('between-tier', frozen.type, error);
            }
        }
        this.runTier(this.catchAll, frozen, ctx);
    }
    veto(kind, handler, opts = {}) {
        if (opts.name !== undefined) {
            const existing = this.byName.get(opts.name);
            if (existing && !opts.override) {
                throw new ProbeNameCollisionError(opts.name);
            }
            if (existing && opts.override)
                this.removeAnywhere(existing);
        }
        const entry = {
            id: this.nextId++,
            name: opts.name,
            priority: opts.priority ?? 0,
            handler: handler,
            where: opts.where,
        };
        let bucket = this.vetoByKind.get(kind);
        if (!bucket) {
            bucket = [];
            this.vetoByKind.set(kind, bucket);
        }
        insertSortedVeto(bucket, entry);
        if (entry.name)
            this.byName.set(entry.name, entry);
        return () => {
            const list = this.vetoByKind.get(kind);
            if (list)
                removeVeto(list, entry);
            if (entry.name)
                this.byName.delete(entry.name);
        };
    }
    queryVeto(event, ctx) {
        const wide = event;
        const frozen = Object.isFrozen(wide) ? wide : Object.freeze(wide);
        const bucket = this.vetoByKind.get(frozen.type);
        if (!bucket || bucket.length === 0)
            return { action: 'allow' };
        let firstDeny;
        for (const entry of bucket) {
            if (entry.where && !entry.where(frozen))
                continue;
            let decision;
            try {
                decision = entry.handler(frozen, ctx);
            }
            catch (error) {
                this.logThrow(entry.name ?? 'unnamed', frozen.type, error);
                continue;
            }
            const normalized = normalizeDecision(decision);
            if (!normalized.allow && firstDeny === undefined) {
                firstDeny = {
                    action: 'deny',
                    probeName: entry.name,
                    reason: normalized.reason,
                };
            }
        }
        return firstDeny ?? { action: 'allow' };
    }
    clear() {
        this.typedByKind.clear();
        this.vetoByKind.clear();
        this.catchAll.length = 0;
        this.byName.clear();
    }
    runTier(entries, event, ctx) {
        for (const entry of entries) {
            if (entry.where && !entry.where(event))
                continue;
            try {
                entry.handler(event, ctx);
            }
            catch (error) {
                this.logThrow(entry.name ?? 'unnamed', event.type, error);
            }
        }
    }
    makeEntry(handler, opts) {
        if (opts.name !== undefined) {
            const existing = this.byName.get(opts.name);
            if (existing && !opts.override) {
                throw new ProbeNameCollisionError(opts.name);
            }
            if (existing && opts.override)
                this.removeAnywhere(existing);
        }
        const entry = {
            id: this.nextId++,
            name: opts.name,
            priority: opts.priority ?? 0,
            handler,
            where: opts.where,
        };
        if (entry.name)
            this.byName.set(entry.name, entry);
        return entry;
    }
    removeAnywhere(entry) {
        for (const bucket of this.typedByKind.values())
            removeEntry(bucket, entry);
        removeEntry(this.catchAll, entry);
        for (const bucket of this.vetoByKind.values())
            removeVeto(bucket, entry);
        if (entry.name)
            this.byName.delete(entry.name);
    }
    logThrow(probeName, eventType, error) {
        if (!this.log)
            return;
        this.log.error('probe handler threw', {
            probeName,
            eventType,
            error: error instanceof Error ? error.message : String(error),
        });
    }
}
export const probe = new ProbeRegistry();
export function createProbeRegistry() {
    return new ProbeRegistry();
}
//# sourceMappingURL=registry.js.map