import { DEFAULT_HOTSPOT_WINDOW_MS, DEFAULT_HOTSPOT_MAX_SAMPLES_PER_KEY } from './types.js';
/**
 * SelectorHotspotSampler — sliding-window latency + frequency tracker.
 *
 * Thread-safety note: synchronous JS — no concurrency concerns.
 */
export class SelectorHotspotSampler {
    _windowMs;
    _maxSamplesPerKey;
    _selectors = new Map();
    /**
     * @param config — Optional sampler configuration.
     */
    constructor(config = {}) {
        this._windowMs = config.windowMs ?? DEFAULT_HOTSPOT_WINDOW_MS;
        this._maxSamplesPerKey = config.maxSamplesPerKey ?? DEFAULT_HOTSPOT_MAX_SAMPLES_PER_KEY;
    }
    // ── Public API ───────────────────────────────────────────────────────────
    /** Current sliding window duration in ms. */
    get windowMs() {
        return this._windowMs;
    }
    /**
     * Record a selector invocation.
     *
     * @param key — Selector name / identifier.
     * @param durationMs — Execution time in milliseconds (may be 0 for sync).
     */
    record(key, durationMs) {
        const now = Date.now();
        let state = this._selectors.get(key);
        if (!state) {
            state = { key, samples: [], totalCalls: 0, totalDurationMs: 0 };
            this._selectors.set(key, state);
        }
        // Lazy GC: drop samples outside the window
        this._evictOldSamples(state, now);
        // Enforce per-key sample cap (drop oldest when over limit)
        if (state.samples.length >= this._maxSamplesPerKey) {
            state.samples.shift();
        }
        state.samples.push({ ts: now, durationMs });
        state.totalCalls++;
        state.totalDurationMs += durationMs;
    }
    /**
     * Return a sorted hotspot report.
     *
     * Hotspots are sorted by `callsInWindow` descending (churn first),
     * then by `p95Ms` descending as a tiebreaker.
     *
     * @returns HotspotReport.
     */
    getReport() {
        const now = Date.now();
        const hotspots = [];
        for (const state of this._selectors.values()) {
            this._evictOldSamples(state, now);
            hotspots.push(this._computeHotspot(state, now));
        }
        hotspots.sort((a, b) => b.callsInWindow - a.callsInWindow || b.p95Ms - a.p95Ms);
        return {
            generatedAt: now,
            windowMs: this._windowMs,
            hotspots,
        };
    }
    /**
     * Return the hotspot for a single selector key, or undefined if never recorded.
     *
     * @param key — Selector identifier.
     */
    getHotspot(key) {
        const state = this._selectors.get(key);
        if (!state)
            return undefined;
        const now = Date.now();
        this._evictOldSamples(state, now);
        return this._computeHotspot(state, now);
    }
    /**
     * Return the top N selectors by call count in the current window.
     *
     * @param n — Maximum number of hotspots to return.
     */
    getTopHotspots(n) {
        return this.getReport().hotspots.slice(0, n);
    }
    /** Number of distinct selector keys tracked. */
    get trackedKeyCount() {
        return this._selectors.size;
    }
    /**
     * Clear all recorded samples and reset tracking.
     * Does not reset configuration.
     */
    reset() {
        this._selectors.clear();
    }
    // ── Private helpers ───────────────────────────────────────────────────────
    _evictOldSamples(state, now) {
        const cutoff = now - this._windowMs;
        // Samples are chronological oldest-first; trim from front
        let i = 0;
        while (i < state.samples.length && (state.samples[i]?.ts ?? 0) < cutoff) {
            i++;
        }
        if (i > 0) {
            state.samples = state.samples.slice(i);
        }
    }
    _computeHotspot(state, now) {
        const windowSec = this._windowMs / 1_000;
        const samples = state.samples;
        const callsInWindow = samples.length;
        // Calls-per-second in window
        const callsPerSecond = windowSec > 0 ? callsInWindow / windowSec : 0;
        // Compute latency percentiles from windowed samples
        const { p50, p95, p99, avgMs, maxMs } = this._percentiles(samples);
        return {
            key: state.key,
            callsInWindow,
            callsPerSecond: Math.round(callsPerSecond * 100) / 100,
            totalCalls: state.totalCalls,
            avgMs,
            p50Ms: p50,
            p95Ms: p95,
            p99Ms: p99,
            maxMs,
            isChurnHotspot: callsPerSecond > 10, // >10 calls/sec
            isLatencyHotspot: p95 > 5, // p95 > 5ms
        };
    }
    _percentiles(samples) {
        if (samples.length === 0) {
            return { p50: 0, p95: 0, p99: 0, avgMs: 0, maxMs: 0 };
        }
        const durations = samples.map((s) => s.durationMs).sort((a, b) => a - b);
        const len = durations.length;
        const sum = durations.reduce((acc, v) => acc + v, 0);
        const pAt = (pct) => {
            const idx = Math.min(Math.ceil(len * pct) - 1, len - 1);
            return Math.round((durations[Math.max(idx, 0)] ?? 0) * 100) / 100;
        };
        return {
            p50: pAt(0.5),
            p95: pAt(0.95),
            p99: pAt(0.99),
            avgMs: Math.round((sum / len) * 100) / 100,
            maxMs: Math.round((durations[len - 1] ?? 0) * 100) / 100,
        };
    }
}
