///|
/// Aggregate facts about one plan.
///
/// The audit answers "what is wrong?"; a summary answers "what does this plan
/// look like?": how many records of each kind, how much calendar time they
/// occupy, how much work they represent, which risk levels they carry, how
/// crowded the busiest minute is and how the load spreads over services.
///
/// Everything is derived from the exported helpers, so a summary can never
/// disagree with the audit and the concurrency measurements taken separately.

///|
/// What one service contributes to a plan.
///
/// The empty service name stands for the changes that name no service; it is
/// reported as `(none)` in text output.
pub(all) struct ServiceSummary {
  service : String
  changes : Int
  blocked_minutes : Int
  peak_concurrency : Int
  high_risk_changes : Int
}

///|
/// Render as
/// `orders-db: 2 changes, 1h30m blocked, peak 2, 1 high risk change`.
///
/// A service with a single change reads `1 change`, not `1 changes`.
pub fn ServiceSummary::to_line(self : ServiceSummary) -> String {
  let name = if self.service == "" { "(none)" } else { self.service }
  name +
  ": " +
  count_text(self.changes, "change", "changes") +
  ", " +
  format_duration(self.blocked_minutes) +
  " blocked, peak " +
  self.peak_concurrency.to_string() +
  ", " +
  count_text(self.high_risk_changes, "high risk change", "high risk changes")
}

///|
/// Everything a single glance should tell about a plan.
///
/// `blocked_minutes` is calendar time (overlaps counted once), while
/// `change_minutes` is workload (every change counted on its own), so the two
/// differ exactly by the overlap inside the plan.
pub(all) struct PlanSummary {
  changes : Int
  freezes : Int
  blocked_minutes : Int
  change_minutes : Int
  low_risk : Int
  medium_risk : Int
  high_risk : Int
  earliest : Int?
  latest : Int?
  span_minutes : Int
  peak : PeakConcurrency?
  services : Array[ServiceSummary]
}

///|
/// Union length of one service's changes: overlapping changes of that service
/// are counted once, and freezes are not part of it.
fn service_blocked_minutes(plan : Plan, service : String) -> Int {
  let slots : Array[Slot] = []
  for change in plan.changes() {
    if change.service == service {
      slots.push({ start: change.start, end: change.end, })
    }
  }
  total_duration(merge_slots(slots))
}

///|
/// Summarise a plan.
///
/// Services are ordered by name with code point comparison, so the empty
/// service sorts first and the output never depends on the input order.
pub fn summarize(plan : Plan) -> PlanSummary {
  let changes = plan.changes()
  let freezes = plan.freezes()
  let mut low_risk = 0
  let mut medium_risk = 0
  let mut high_risk = 0
  for change in changes {
    match change.risk {
      Risk::Low => low_risk = low_risk + 1
      Risk::Medium => medium_risk = medium_risk + 1
      Risk::High => high_risk = high_risk + 1
    }
  }
  let mut earliest : Int? = None
  let mut latest : Int? = None
  for window in plan.windows {
    match earliest {
      None => earliest = Some(window.start)
      Some(current) =>
        if window.start < current {
          earliest = Some(window.start)
        }
    }
    match latest {
      None => latest = Some(window.end)
      Some(current) => if window.end > current { latest = Some(window.end) }
    }
  }
  let span = match (earliest, latest) {
    (Some(from), Some(to)) => to - from
    _ => 0
  }
  let names : Array[String] = []
  for change in changes {
    names.push(change.service)
  }
  let services : Array[ServiceSummary] = []
  for service in distinct_strings(names) {
    let mut high = 0
    let mut count = 0
    for change in changes {
      if change.service == service {
        count = count + 1
        if change.risk == Risk::High {
          high = high + 1
        }
      }
    }
    services.push({
      service,
      changes: count,
      blocked_minutes: service_blocked_minutes(plan, service),
      peak_concurrency: service_peak(plan, service),
      high_risk_changes: high,
    })
  }
  {
    changes: changes.length(),
    freezes: freezes.length(),
    blocked_minutes: blocked_minutes(plan),
    change_minutes: change_minutes(plan),
    low_risk,
    medium_risk,
    high_risk,
    earliest,
    latest,
    span_minutes: span,
    peak: peak_concurrency(plan),
    services,
  }
}

///|
/// Render a summary as a stable text block.
///
/// The shape is fixed: a block of `key: value` lines, then a `SERVICES`
/// section with one two-space-indented line per service or `  (none)`. The text
/// ends with exactly one newline.
pub fn render_summary(summary : PlanSummary) -> String {
  let builder = StringBuilder()
  builder.write_string("Plan summary\n")
  builder.write_string("changes: " + summary.changes.to_string() + "\n")
  builder.write_string("freezes: " + summary.freezes.to_string() + "\n")
  builder.write_string(
    "risks: low " +
    summary.low_risk.to_string() +
    ", medium " +
    summary.medium_risk.to_string() +
    ", high " +
    summary.high_risk.to_string() +
    "\n",
  )
  builder.write_string(
    "blocked: " + format_duration(summary.blocked_minutes) + "\n",
  )
  builder.write_string(
    "workload: " + format_duration(summary.change_minutes) + "\n",
  )
  match (summary.earliest, summary.latest) {
    (Some(from), Some(to)) =>
      builder.write_string(
        "span: " + format_instant(from) + " -> " + format_instant(to) + "\n",
      )
    _ => builder.write_string("span: (empty)\n")
  }
  match summary.peak {
    Some(peak) => builder.write_string("peak: " + peak.to_line() + "\n")
    None => builder.write_string("peak: (none)\n")
  }
  builder.write_string(
    "services: " + summary.services.length().to_string() + "\n",
  )
  builder.write_string("\nSERVICES\n")
  if summary.services.length() == 0 {
    builder.write_string("  (none)\n")
  } else {
    for service in summary.services {
      builder.write_string("  ")
      builder.write_string(service.to_line())
      builder.write_char('\n')
    }
  }
  builder.to_string()
}

///|
/// The summary of one service, or `None` when the name is not in the plan.
pub fn find_service(summary : PlanSummary, service : String) -> ServiceSummary? {
  for entry in summary.services {
    if entry.service == service {
      return Some(entry)
    }
  }
  None
}