///|
const TREND_HOUR_SECONDS : Int = 60 * 60

///|
const TREND_DAY_SECONDS : Int = 24 * 60 * 60

///|
fn TrafficStats::usage_trends_json(
  self : TrafficStats,
  start_ts? : Int? = None,
  end_ts? : Int? = None,
  app_type? : String? = None,
) -> Json {
  let normalized = normalize_trend_range(start_ts, end_ts)
  if normalized.duration <= TREND_DAY_SECONDS {
    self.hourly_usage_trends_json(
      normalized.start_ts,
      normalized.end_ts,
      app_type,
    )
  } else {
    self.daily_usage_trends_json(
      normalized.start_ts,
      normalized.end_ts,
      app_type,
    )
  }
}

///|
priv struct TrendRange {
  start_ts : Int
  end_ts : Int
  duration : Int
}

///|
priv struct DailyStatsAccumulator {
  date : String
  mut request_count : Int
  mut total_cost_nano : Int64
  mut total_tokens : Int
  mut total_input_tokens : Int
  mut total_output_tokens : Int
  mut total_cache_creation_tokens : Int
  mut total_cache_read_tokens : Int
}

///|
fn DailyStatsAccumulator::new(date : String) -> DailyStatsAccumulator {
  {
    date,
    request_count: 0,
    total_cost_nano: 0L,
    total_tokens: 0,
    total_input_tokens: 0,
    total_output_tokens: 0,
    total_cache_creation_tokens: 0,
    total_cache_read_tokens: 0,
  }
}

///|
fn DailyStatsAccumulator::add(
  self : DailyStatsAccumulator,
  log : RequestLog,
) -> Unit {
  let fresh_input_tokens = request_log_fresh_input_tokens(log)
  self.request_count += 1
  self.total_cost_nano += request_log_total_cost_nano(log)
  self.total_tokens += fresh_input_tokens + log.output_tokens
  self.total_input_tokens += fresh_input_tokens
  self.total_output_tokens += log.output_tokens
  self.total_cache_creation_tokens += log.cache_creation_tokens
  self.total_cache_read_tokens += log.cache_read_tokens
}

///|
fn DailyStatsAccumulator::add_rollup(
  self : DailyStatsAccumulator,
  rollup : UsageDailyRollup,
) -> Unit {
  let fresh_input_tokens = usage_daily_rollup_fresh_input_tokens(rollup)
  self.request_count += rollup.request_count
  self.total_cost_nano += rollup.total_cost_nano
  self.total_tokens += fresh_input_tokens + rollup.output_tokens
  self.total_input_tokens += fresh_input_tokens
  self.total_output_tokens += rollup.output_tokens
  self.total_cache_creation_tokens += rollup.cache_creation_tokens
  self.total_cache_read_tokens += rollup.cache_read_tokens
}

///|
fn DailyStatsAccumulator::to_json(self : DailyStatsAccumulator) -> Json {
  {
    "date": Json::string(self.date),
    "requestCount": Json::number(self.request_count.to_double()),
    "totalCost": Json::string(format_nano_usd_fixed_6(self.total_cost_nano)),
    "totalTokens": Json::number(self.total_tokens.to_double()),
    "totalInputTokens": Json::number(self.total_input_tokens.to_double()),
    "totalOutputTokens": Json::number(self.total_output_tokens.to_double()),
    "totalCacheCreationTokens": Json::number(
      self.total_cache_creation_tokens.to_double(),
    ),
    "totalCacheReadTokens": Json::number(
      self.total_cache_read_tokens.to_double(),
    ),
  }.to_json()
}

///|
fn normalize_trend_range(start_ts : Int?, end_ts : Int?) -> TrendRange {
  let end_ts = end_ts.unwrap_or(trend_now_seconds())
  let mut start_ts = start_ts.unwrap_or(end_ts - TREND_DAY_SECONDS)
  if start_ts >= end_ts {
    start_ts = end_ts - TREND_DAY_SECONDS
  }
  { start_ts, end_ts, duration: end_ts - start_ts }
}

///|
fn trend_now_seconds() -> Int {
  @os.unix_seconds()
}

///|
fn TrafficStats::hourly_usage_trends_json(
  self : TrafficStats,
  start_ts : Int,
  end_ts : Int,
  app_type : String?,
) -> Json {
  let bucket_count = trend_bucket_count(end_ts - start_ts, TREND_HOUR_SECONDS)
  let buckets : Array[DailyStatsAccumulator] = []
  for i in 0..= bucket_count {
      bucket_index = bucket_count - 1
    }
    buckets[bucket_index].add(log)
  }
  Json::array(buckets.map(fn(bucket) { bucket.to_json() }))
}

///|
fn TrafficStats::daily_usage_trends_json(
  self : TrafficStats,
  start_ts : Int,
  end_ts : Int,
  app_type : String?,
) -> Json {
  let buckets : Array[DailyStatsAccumulator] = []
  let bucket_indices : Map[String, Int] = Map([], capacity=0)
  let end_day_start = trend_local_day_start_ts(end_ts)
  let mut bucket_start = trend_local_day_start_ts(start_ts)
  while bucket_start <= end_day_start {
    let key = trend_local_day_key(bucket_start)
    bucket_indices.set(key, buckets.length())
    buckets.push(
      DailyStatsAccumulator::new(trend_timestamp_rfc3339(bucket_start)),
    )
    let next_bucket_start = trend_local_next_day_start_ts(bucket_start)
    if next_bucket_start <= bucket_start {
      break
    }
    bucket_start = next_bucket_start
  }
  if buckets.length() == 0 {
    let key = trend_local_day_key(start_ts)
    bucket_indices.set(key, 0)
    buckets.push(DailyStatsAccumulator::new(trend_timestamp_rfc3339(start_ts)))
  }
  for log in self.request_logs {
    if !self.request_log_is_effective(log) ||
      !request_log_matches_trend(log, start_ts, end_ts, app_type) {
      continue
    }
    match bucket_indices.get(trend_local_day_key(log.created_at)) {
      Some(bucket_index) => buckets[bucket_index].add(log)
      None => ()
    }
  }
  for rollup in self.daily_rollups {
    if !usage_daily_rollup_matches_aggregate(
        rollup,
        Some(start_ts),
        Some(end_ts),
        app_type,
      ) {
      continue
    }
    match bucket_indices.get(rollup.date) {
      Some(bucket_index) => buckets[bucket_index].add_rollup(rollup)
      None => ()
    }
  }
  Json::array(buckets.map(fn(bucket) { bucket.to_json() }))
}

///|
fn request_log_matches_trend(
  log : RequestLog,
  start_ts : Int,
  end_ts : Int,
  app_type : String?,
) -> Bool {
  log.created_at >= start_ts &&
  log.created_at <= end_ts &&
  request_log_matches_app(log, app_type)
}

///|
fn trend_bucket_count(duration : Int, bucket_seconds : Int) -> Int {
  max_int((max_int(duration, 1) + bucket_seconds - 1) / bucket_seconds, 1)
}

///|
fn floor_div_int(value : Int, divisor : Int) -> Int {
  if value >= 0 {
    value / divisor
  } else {
    (value - divisor + 1) / divisor
  }
}

///|
fn trend_local_day_start_ts(timestamp : Int) -> Int {
  @os.local_day_start_seconds(timestamp)
}

///|
fn trend_local_next_day_start_ts(timestamp : Int) -> Int {
  @os.local_next_day_start_seconds(timestamp)
}

///|
fn trend_local_day_key(timestamp : Int) -> String {
  @os.local_date_key(timestamp.to_int64())
}

///|
fn trend_timestamp_rfc3339(timestamp : Int) -> String {
  @os.local_rfc3339(timestamp.to_int64())
}