import { assert } from "../../../core/assert.js";

/**
 * Aggregates `getStats()` snapshots from one or more named sources (typically
 * a `Transport`, `Channel`, or `NetworkPeer.channel_for(peer)`) and reports
 * recent throughput rates.
 *
 * Per-source stats are queried via the source's `getStats()` method. A "sample"
 * captures the current totals at a wall-clock moment; the meter retains a
 * sliding window of samples and computes bytes-per-second / packets-per-second
 * over the window.
 *
 * No timer of its own — caller invokes {@link sample} per tick (or whenever).
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class BandwidthMeter {
    #sources;
    #samples;


    /**
     * @param {{ window_seconds?: number }} [options]
     */
    constructor({ window_seconds = 5 } = {}) {
        assert.isNumber(window_seconds, 'window_seconds');
        if (window_seconds <= 0) throw new Error('BandwidthMeter: window_seconds must be > 0');

        /** @readonly */
        this.window_seconds = window_seconds;

        /**
         * Registered sources. Each is { name: string, source: { getStats() } }.
         * @type {{name: string, source: any}[]}
         * @private
         */
        this.#sources = [];

        /**
         * Per-window samples (oldest first). Each entry: { time_ms, totals }.
         * @type {{time_ms: number, totals: {bytes_in: number, bytes_out: number, packets_in: number, packets_out: number}}[]}
         * @private
         */
        this.#samples = [];
    }

    /**
     * @param {string} name human-readable label
     * @param {{ getStats(): {bytes_in: number, bytes_out: number, packets_in: number, packets_out: number} }} source
     */
    add_source(name, source) {
        assert.isString(name, 'name');
        assert.ok(source && typeof source.getStats === 'function', 'source must implement getStats()');
        this.#sources.push({ name, source });
    }

    /**
     * Capture a fresh sample at the given wall-clock time (caller-supplied so
     * tests can use a deterministic clock; in production typically `Date.now()`).
     *
     * @param {number} now_ms
     */
    sample(now_ms) {
        assert.isNumber(now_ms, 'now_ms');
        const totals = this.#sum_totals();
        this.#samples.push({ time_ms: now_ms, totals });

        // Drop samples older than the window, but keep one straddling expired
        // sample if it's the only thing standing between us and "only the
        // just-pushed sample is left" (which would silently make __rate return
        // 0 after a long quiet stretch). Once we have 2+ in-window samples the
        // straddle is no longer needed and would bias the rate, so drop it.
        const cutoff = now_ms - this.window_seconds * 1000;
        while (this.#samples.length >= 2 && this.#samples[1].time_ms < cutoff) {
            this.#samples.shift();
        }
        if (this.#samples.length >= 3 && this.#samples[0].time_ms < cutoff) {
            this.#samples.shift();
        }
    }

    /**
     * Cumulative totals across all sources, right now (re-queries getStats()).
     * Independent of the sample window.
     */
    cumulative() {
        return this.#sum_totals();
    }

    /**
     * Bytes-per-second received, averaged over the sample window.
     * Returns 0 if fewer than 2 samples have been taken.
     */
    rate_bytes_in() {
        return this.#rate('bytes_in');
    }
    rate_bytes_out() {
        return this.#rate('bytes_out');
    }
    rate_packets_in() {
        return this.#rate('packets_in');
    }
    rate_packets_out() {
        return this.#rate('packets_out');
    }

    /**
     * Per-source breakdown of cumulative totals (re-queries getStats()).
     * @returns {Array<{name: string, totals: object}>}
     */
    per_source() {
        const result = [];
        for (let i = 0; i < this.#sources.length; i++) {
            const s = this.#sources[i];
            result.push({ name: s.name, totals: s.source.getStats() });
        }
        return result;
    }

    /**
     * Drop all retained samples. Source registrations are preserved.
     */
    reset_samples() {
        this.#samples.length = 0;
    }

    /**
     * @private
     */
    #sum_totals() {
        let bytes_in = 0, bytes_out = 0, packets_in = 0, packets_out = 0;
        for (let i = 0; i < this.#sources.length; i++) {
            const s = this.#sources[i].source.getStats();
            bytes_in += s.bytes_in;
            bytes_out += s.bytes_out;
            packets_in += s.packets_in;
            packets_out += s.packets_out;
        }
        return { bytes_in, bytes_out, packets_in, packets_out };
    }

    /**
     * @private
     */
    #rate(key) {
        const n = this.#samples.length;
        if (n < 2) return 0;
        const oldest = this.#samples[0];
        const newest = this.#samples[n - 1];
        const dt_seconds = (newest.time_ms - oldest.time_ms) / 1000;
        if (dt_seconds <= 0) return 0;
        return (newest.totals[key] - oldest.totals[key]) / dt_seconds;
    }
}
