///|
/// Controls how many historical snapshots remain after compaction.
/// Daily and weekly windows are measured backwards from the newest snapshot.
pub(all) struct HistoryRetentionPolicy {
  format_version : Int
  keep_latest : Int
  keep_daily_days : Int
  keep_weekly_weeks : Int
  maximum_observations : Int
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct RetentionDecision {
  original_observations : Int
  kept_observations : Int
  removed_observations : Int
  kept_created_at : Array[Int64]
  removed_created_at : Array[Int64]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn HistoryRetentionPolicy::default() -> HistoryRetentionPolicy {
  HistoryRetentionPolicy::{
    format_version: 1,
    keep_latest: 10,
    keep_daily_days: 30,
    keep_weekly_weeks: 26,
    maximum_observations: 100,
  }
}

///|
pub fn validate_retention_policy(
  policy : HistoryRetentionPolicy,
) -> Array[String] {
  let issues : Array[String] = []
  if policy.format_version != 1 {
    issues.push("Unsupported retention policy version.")
  }
  if policy.keep_latest < 1 {
    issues.push("keep_latest must be at least one.")
  }
  if policy.keep_daily_days < 0 {
    issues.push("keep_daily_days cannot be negative.")
  }
  if policy.keep_weekly_weeks < 0 {
    issues.push("keep_weekly_weeks cannot be negative.")
  }
  if policy.maximum_observations < 1 {
    issues.push("maximum_observations must be at least one.")
  }
  if policy.keep_latest > policy.maximum_observations {
    issues.push("keep_latest cannot exceed maximum_observations.")
  }
  issues
}

///|
fn retention_day(timestamp : Int64) -> Int64 {
  timestamp / 86400L
}

///|
fn retention_week(timestamp : Int64) -> Int64 {
  timestamp / 604800L
}

///|
fn selected_count(selected : Map[Int64, Bool]) -> Int {
  let mut count = 0
  for _, keep in selected {
    if keep {
      count += 1
    }
  }
  count
}

///|
fn choose_retention_time(
  selected : Map[Int64, Bool],
  created_at : Int64,
  maximum : Int,
) -> Unit {
  if selected_count(selected) < maximum {
    selected[created_at] = true
  }
}

///|
/// Select snapshots deterministically. The newest observations are protected
/// first, followed by one newest observation for each daily and weekly bucket.
pub fn plan_history_retention(
  history : SnapshotHistory,
  policy : HistoryRetentionPolicy,
) -> RetentionDecision raise {
  let issues = validate_retention_policy(policy)
  if !issues.is_empty() {
    raise Failure("Invalid retention policy: " + issues[0])
  }
  let selected : Map[Int64, Bool] = Map([])
  let seen_days : Map[Int64, Bool] = Map([])
  let seen_weeks : Map[Int64, Bool] = Map([])
  let length = history.snapshots.length()
  let newest_time = if length == 0 {
    0L
  } else {
    history.snapshots[length - 1].created_at
  }
  for offset = 0; offset < length; offset = offset + 1 {
    let index = length - 1 - offset
    let timestamp = history.snapshots[index].created_at
    if offset < policy.keep_latest {
      choose_retention_time(selected, timestamp, policy.maximum_observations)
      seen_days[retention_day(timestamp)] = true
      seen_weeks[retention_week(timestamp)] = true
      continue
    }
    let age = newest_time - timestamp
    if age <= policy.keep_daily_days.to_int64() * 86400L {
      let day = retention_day(timestamp)
      if !seen_days.contains(day) {
        seen_days[day] = true
        choose_retention_time(selected, timestamp, policy.maximum_observations)
      }
    } else if age <= policy.keep_weekly_weeks.to_int64() * 604800L {
      let week = retention_week(timestamp)
      if !seen_weeks.contains(week) {
        seen_weeks[week] = true
        choose_retention_time(selected, timestamp, policy.maximum_observations)
      }
    }
  }
  let kept : Array[Int64] = []
  let removed : Array[Int64] = []
  for snapshot in history.snapshots {
    if selected.contains(snapshot.created_at) {
      kept.push(snapshot.created_at)
    } else {
      removed.push(snapshot.created_at)
    }
  }
  RetentionDecision::{
    original_observations: length,
    kept_observations: kept.length(),
    removed_observations: removed.length(),
    kept_created_at: kept,
    removed_created_at: removed,
  }
}

///|
pub fn compact_history(
  history : SnapshotHistory,
  decision : RetentionDecision,
) -> SnapshotHistory {
  let selected : Map[Int64, Bool] = Map([])
  for timestamp in decision.kept_created_at {
    selected[timestamp] = true
  }
  let snapshots = history.snapshots.filter(fn(snapshot) {
    selected.contains(snapshot.created_at)
  })
  { ..history, snapshots, }
}

///|
pub fn render_retention_decision(decision : RetentionDecision) -> String {
  [
    "MoonDisk history retention plan",
    "Original observations: " + decision.original_observations.to_string(),
    "Kept observations: " + decision.kept_observations.to_string(),
    "Removed observations: " + decision.removed_observations.to_string(),
    "Oldest kept timestamp: " +
    (if decision.kept_created_at.is_empty() {
      "none"
    } else {
      decision.kept_created_at[0].to_string()
    }),
    "Newest kept timestamp: " +
    (if decision.kept_created_at.is_empty() {
      "none"
    } else {
      decision.kept_created_at[decision.kept_created_at.length() - 1].to_string()
    }),
  ].join("\n")
}

///|
pub fn retention_policy_to_json(policy : HistoryRetentionPolicy) -> String {
  ToJson::to_json(policy).stringify(indent=2)
}

///|
pub fn retention_policy_from_json(
  source : String,
) -> HistoryRetentionPolicy raise {
  let policy : HistoryRetentionPolicy = @json.from_json(@json.parse(source))
  let issues = validate_retention_policy(policy)
  if !issues.is_empty() {
    raise Failure("Invalid retention policy: " + issues[0])
  }
  policy
}

///|
pub async fn write_retention_policy(
  path : String,
  policy : HistoryRetentionPolicy,
) -> Unit {
  @fs.write_file(path, retention_policy_to_json(policy))
}

///|
pub async fn read_retention_policy(path : String) -> HistoryRetentionPolicy {
  retention_policy_from_json(@fs.read_file(path).text())
}