///|
pub(all) enum InsightSeverity {
  Info
  Notice
  Warning
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) enum InsightKind {
  ScanIncomplete
  VeryLargeFile
  DominantDirectory
  StaleStorage
  RegenerableOutput
  EmptyDirectories
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct StorageInsight {
  kind : InsightKind
  severity : InsightSeverity
  title : String
  detail : String
  path : String
  size : Int64
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct InsightOptions {
  large_file_bytes : Int64
  large_file_share_tenths : Int
  dominant_directory_share_tenths : Int
  stale_bytes : Int64
  max_large_file_insights : Int
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn InsightOptions::default() -> InsightOptions {
  InsightOptions::{
    large_file_bytes: 100L * 1024L * 1024L,
    large_file_share_tenths: 250,
    dominant_directory_share_tenths: 600,
    stale_bytes: 500L * 1024L * 1024L,
    max_large_file_insights: 5,
  }
}

///|
fn share_tenths(part : Int64, total : Int64) -> Int {
  if total <= 0L {
    0
  } else {
    (part * 1000L / total).to_int()
  }
}

///|
fn severity_name(severity : InsightSeverity) -> String {
  match severity {
    Info => "info"
    Notice => "notice"
    Warning => "warning"
  }
}

///|
fn large_file_insights(
  snapshot : Snapshot,
  options : InsightOptions,
) -> Array[StorageInsight] {
  let result : Array[StorageInsight] = []
  let total = snapshot.total_size()
  for entry in largest_files(snapshot, options.max_large_file_insights) {
    let share = share_tenths(entry.size, total)
    if entry.size >= options.large_file_bytes ||
      share >= options.large_file_share_tenths {
      result.push(StorageInsight::{
        kind: VeryLargeFile,
        severity: if share >= 500 {
          Warning
        } else {
          Notice
        },
        title: "Large individual file",
        detail: entry.path +
        " uses " +
        format_bytes(entry.size) +
        " (" +
        format_percent(entry.size, total) +
        ") of the scanned data.",
        path: entry.path,
        size: entry.size,
      })
    }
  }
  result
}

///|
fn dominant_directory_insights(
  snapshot : Snapshot,
  options : InsightOptions,
) -> Array[StorageInsight] {
  let result : Array[StorageInsight] = []
  let total = snapshot.total_size()
  for stat in directory_stats(snapshot) {
    if stat.path != "" &&
      path_depth(stat.path) == 1 &&
      share_tenths(stat.recursive_size, total) >=
      options.dominant_directory_share_tenths {
      result.push(StorageInsight::{
        kind: DominantDirectory,
        severity: Notice,
        title: "Directory dominates this scan",
        detail: stat.path +
        " accounts for " +
        format_percent(stat.recursive_size, total) +
        " of all file bytes.",
        path: stat.path,
        size: stat.recursive_size,
      })
    }
  }
  result
}

///|
fn regenerable_insights(
  snapshot : Snapshot,
  now : Int64,
) -> Array[StorageInsight] {
  let result : Array[StorageInsight] = []
  let matches = evaluate_rules(builtin_cleanup_rules(), snapshot, now)
  for candidate in matches {
    if candidate.regenerable {
      result.push(StorageInsight::{
        kind: RegenerableOutput,
        severity: Info,
        title: candidate.rule_title,
        detail: candidate.reason,
        path: candidate.path,
        size: candidate.size,
      })
    }
  }
  result
}

///|
pub fn storage_insights(
  snapshot : Snapshot,
  now : Int64,
  options? : InsightOptions = InsightOptions::default(),
) -> Array[StorageInsight] {
  let result : Array[StorageInsight] = []
  if !snapshot.errors.is_empty() {
    result.push(StorageInsight::{
      kind: ScanIncomplete,
      severity: Warning,
      title: "Scan completed with metadata errors",
      detail: snapshot.errors.length().to_string() +
      " paths could not be inspected, so totals may be incomplete.",
      path: "",
      size: 0L,
    })
  }
  result.append(large_file_insights(snapshot, options))
  result.append(dominant_directory_insights(snapshot, options))
  let ages = age_buckets(snapshot, now)
  if ages.older.size >= options.stale_bytes {
    result.push(StorageInsight::{
      kind: StaleStorage,
      severity: Notice,
      title: "Substantial storage is older than one year",
      detail: format_bytes(ages.older.size) +
      " across " +
      ages.older.files.to_string() +
      " files has not been modified for at least 365 days.",
      path: "",
      size: ages.older.size,
    })
  }
  result.append(regenerable_insights(snapshot, now))
  let empty = empty_directories(snapshot)
  if !empty.is_empty() {
    result.push(StorageInsight::{
      kind: EmptyDirectories,
      severity: Info,
      title: "Empty directories found",
      detail: empty.length().to_string() +
      " directories contain no regular files.",
      path: empty[0].path,
      size: 0L,
    })
  }
  result.sort_by(fn(a, b) {
    let a_rank = match a.severity {
      Warning => 0
      Notice => 1
      Info => 2
    }
    let b_rank = match b.severity {
      Warning => 0
      Notice => 1
      Info => 2
    }
    if a_rank < b_rank {
      -1
    } else if a_rank > b_rank {
      1
    } else if a.size > b.size {
      -1
    } else if a.size < b.size {
      1
    } else if a.path < b.path {
      -1
    } else {
      1
    }
  })
  result
}

///|
pub fn render_insights(insights : Array[StorageInsight]) -> String {
  let lines : Array[String] = ["MoonDisk storage insights"]
  if insights.is_empty() {
    lines.push("No notable storage patterns were detected.")
  } else {
    for insight in insights {
      let location = if insight.path == "" {
        ""
      } else {
        " [" + insight.path + "]"
      }
      lines.push(
        "[" + severity_name(insight.severity) + "] " + insight.title + location,
      )
      lines.push("  " + insight.detail)
    }
  }
  lines.join("\n")
}