/**
 * Throughput tracking for an in-flight transfer.
 *
 * Smooths byte-rate via exponential moving average over the last `windowMs`
 * window, recomputed on every progress sample. Returns `bytesPerSecond` and
 * `etaSeconds` when total size is known.
 */
export interface ProgressSample {
  bytesPerSecond: number;
  etaSeconds: number | undefined;
  percent: number | undefined;
}

export class ProgressTracker {
  private bytesProcessed = 0;
  private startedAt = -1;
  private lastSampleAt = -1;
  private lastBytes = 0;
  /** EMA of byte rate (bytes per second). */
  private rateEma = 0;
  /** Smoothing factor in [0, 1]. Higher = more responsive, less smooth. */
  private readonly alpha: number;

  constructor(
    private readonly bytesTotal: number | undefined,
    /** Window over which EMA decay equals 1 - 1/e (~63 %). */
    windowMs = 5000,
    /** Sample period — sampling more often than this returns the cached EMA. */
    private readonly sampleEveryMs = 250,
  ) {
    // Convert window to per-sample alpha: alpha = 1 - exp(-dt/window)
    // Approximate over `sampleEveryMs` per tick.
    this.alpha = 1 - Math.exp(-this.sampleEveryMs / windowMs);
  }

  /** Record bytes processed since last call. */
  add(bytes: number): void {
    if (this.startedAt < 0) {
      this.startedAt = nowMs();
      this.lastSampleAt = this.startedAt;
    }
    this.bytesProcessed += bytes;
  }

  /** Total bytes accounted-for so far. */
  get totalProcessed(): number {
    return this.bytesProcessed;
  }

  /** Total elapsed wall-clock since first byte. */
  get elapsedMs(): number {
    if (this.startedAt < 0) return 0;
    return nowMs() - this.startedAt;
  }

  /** Compute a smoothed sample. Cheap to call repeatedly. */
  sample(): ProgressSample {
    const now = nowMs();
    if (this.startedAt < 0) {
      return { bytesPerSecond: 0, etaSeconds: undefined, percent: this.percent() };
    }
    const dt = now - this.lastSampleAt;
    if (dt >= this.sampleEveryMs) {
      const deltaBytes = this.bytesProcessed - this.lastBytes;
      const instantRate = dt > 0 ? (deltaBytes * 1000) / dt : 0;
      this.rateEma =
        this.rateEma === 0 ? instantRate : this.alpha * instantRate + (1 - this.alpha) * this.rateEma;
      this.lastSampleAt = now;
      this.lastBytes = this.bytesProcessed;
    }

    const bps = Math.max(0, this.rateEma);
    let etaSeconds: number | undefined;
    if (this.bytesTotal !== undefined && bps > 0) {
      const remaining = Math.max(0, this.bytesTotal - this.bytesProcessed);
      etaSeconds = remaining / bps;
    } else if (this.bytesTotal !== undefined && this.bytesProcessed >= this.bytesTotal) {
      etaSeconds = 0;
    }

    return { bytesPerSecond: bps, etaSeconds, percent: this.percent() };
  }

  private percent(): number | undefined {
    if (this.bytesTotal === undefined) return undefined;
    if (this.bytesTotal === 0) return 1;
    return Math.min(1, this.bytesProcessed / this.bytesTotal);
  }
}

function nowMs(): number {
  return typeof performance !== 'undefined' ? performance.now() : Date.now();
}
