interface ICompressedDataPoint {
  up: number
  down: number
  timestamp: number
  samples: number
}

const pad2 = (value: number) => value.toString().padStart(2, '0')

export const formatTrafficHourMinute = (timestamp: number) => {
  const date = new Date(timestamp)
  return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
}

export const formatTrafficMinuteSecond = (timestamp: number) => {
  const date = new Date(timestamp)
  return `${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`
}

export const formatTrafficName = (timestamp: number) => {
  const date = new Date(timestamp)
  return `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(
    date.getSeconds(),
  )}`
}

export class TrafficDataSampler {
  private rawBuffer: ITrafficDataPoint[] = []
  private rawHead = 0
  private compressedBuffer: ICompressedDataPoint[] = []
  private compressedHead = 0
  private compressionQueue: ITrafficDataPoint[] = []

  constructor(private config: ISamplingConfig) {}

  addDataPoint(point: ITrafficDataPoint) {
    this.rawBuffer.push(point)

    const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000
    // O(1) amortized trimming using moving head; compact occasionally
    while (
      this.rawHead < this.rawBuffer.length &&
      this.rawBuffer[this.rawHead]?.timestamp <= rawCutoff
    ) {
      this.rawHead++
    }
    if (this.rawHead > 512 && this.rawHead > this.rawBuffer.length / 2) {
      this.rawBuffer = this.rawBuffer.slice(this.rawHead)
      this.rawHead = 0
    }

    this.compressionQueue.push(point)
    if (this.compressionQueue.length >= this.config.compressionRatio) {
      this.compressData()
    }

    const compressedCutoff =
      Date.now() - this.config.compressedDataMinutes * 60 * 1000
    while (
      this.compressedHead < this.compressedBuffer.length &&
      this.compressedBuffer[this.compressedHead]?.timestamp <= compressedCutoff
    ) {
      this.compressedHead++
    }
    if (
      this.compressedHead > 256 &&
      this.compressedHead > this.compressedBuffer.length / 2
    ) {
      this.compressedBuffer = this.compressedBuffer.slice(this.compressedHead)
      this.compressedHead = 0
    }
  }

  private compressData() {
    if (this.compressionQueue.length === 0) return

    const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0)
    const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0)
    const avgTimestamp =
      this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) /
      this.compressionQueue.length

    const compressedPoint: ICompressedDataPoint = {
      up: totalUp / this.compressionQueue.length,
      down: totalDown / this.compressionQueue.length,
      timestamp: avgTimestamp,
      samples: this.compressionQueue.length,
    }

    this.compressedBuffer.push(compressedPoint)
    this.compressionQueue = []
  }

  getDataForTimeRange(minutes: number): ITrafficDataPoint[] {
    const cutoff = Date.now() - minutes * 60 * 1000

    let rawStart = this.rawHead
    while (
      rawStart < this.rawBuffer.length &&
      this.rawBuffer[rawStart]?.timestamp <= cutoff
    ) {
      rawStart++
    }
    const rawData = this.rawBuffer.slice(rawStart)

    if (minutes <= this.config.rawDataMinutes) {
      return rawData
    }

    const compressedCutoffUpper =
      Date.now() - this.config.rawDataMinutes * 60 * 1000

    let compressedStart = this.compressedHead
    while (
      compressedStart < this.compressedBuffer.length &&
      this.compressedBuffer[compressedStart]?.timestamp <= cutoff
    ) {
      compressedStart++
    }

    const compressedData = this.compressedBuffer
      .slice(compressedStart)
      .filter((p) => p.timestamp <= compressedCutoffUpper)
      .map((p) => ({
        up: p.up,
        down: p.down,
        timestamp: p.timestamp,
        name: formatTrafficName(p.timestamp),
      }))

    return [...compressedData, ...rawData].sort(
      (a, b) => a.timestamp - b.timestamp,
    )
  }

  getStats(): ISamplerStats {
    return {
      rawBufferSize: this.rawBuffer.length - this.rawHead,
      compressedBufferSize: this.compressedBuffer.length - this.compressedHead,
      compressionQueueSize: this.compressionQueue.length,
      totalMemoryPoints:
        this.rawBuffer.length -
        this.rawHead +
        (this.compressedBuffer.length - this.compressedHead),
    }
  }

  clear() {
    this.rawBuffer = []
    this.rawHead = 0
    this.compressedBuffer = []
    this.compressedHead = 0
    this.compressionQueue = []
  }
}
