///|
/// Offline audit of a change window plan: overlaps and dependency order.

///|
/// Overlap in minutes of two half open intervals; `0` when they only touch.
fn overlap_minutes(
  first_start : Int,
  first_end : Int,
  second_start : Int,
  second_end : Int,
) -> Int {
  let start = if first_start > second_start {
    first_start
  } else {
    second_start
  }
  let end = if first_end < second_end { first_end } else { second_end }
  let length = end - start
  if length > 0 {
    length
  } else {
    0
  }
}

///|
/// Impact of an overlap: two high risk windows are `critical`, one high risk
/// window makes it `major`, everything else is `minor`.
fn impact_of(first : Risk, second : Risk) -> Impact {
  let first_high = first == Risk::High
  let second_high = second == Risk::High
  if first_high && second_high {
    Impact::Critical
  } else if first_high || second_high {
    Impact::Major
  } else {
    Impact::Minor
  }
}

///|
/// Copy windows ordered by start minute, keeping input order for equal starts.
fn sort_windows_by_start(windows : Array[Window]) -> Array[Window] {
  let ordered : Array[Window] = []
  for window in windows {
    ordered.push(window)
  }
  let mut index = 1
  while index < ordered.length() {
    let current = ordered[index]
    let mut position = index
    while position > 0 && ordered[position - 1].start > current.start {
      ordered[position] = ordered[position - 1]
      position = position - 1
    }
    ordered[position] = current
    index = index + 1
  }
  ordered
}

///|
/// Audit a plan.
///
/// Finds every pair of windows that share at least one minute, checks every
/// dependency for a missing target, a self reference and a wrong order, and
/// measures the total blocked time.
///
/// A dependency is satisfied when it ends at or before the dependent starts.
/// Ending exactly at the start is accepted but reported as a `back-to-back`
/// warning, because a change with no slack has no room for overrun.
///
/// Windows that failed to parse are not part of `plan.windows` and therefore
/// cannot overlap or be depended on; their problems stay in
/// `plan.diagnostics`, and the report still fails.
pub fn audit(plan : Plan) -> AuditReport {
  let ordered = sort_windows_by_start(plan.changes())
  let overlaps : Array[Overlap] = []
  let mut first_index = 0
  while first_index < ordered.length() {
    let first = ordered[first_index]
    let mut second_index = first_index + 1
    while second_index < ordered.length() {
      let second = ordered[second_index]
      if second.start >= first.end {
        break
      }
      let minutes = overlap_minutes(
        first.start,
        first.end,
        second.start,
        second.end,
      )
      if minutes > 0 {
        overlaps.push({
          first: first.id,
          second: second.id,
          minutes,
          impact: impact_of(first.risk, second.risk),
        })
      }
      second_index = second_index + 1
    }
    first_index = first_index + 1
  }
  let issues : Array[DependencyIssue] = []
  for window in plan.changes() {
    for dependency in window.depends {
      if dependency == window.id {
        issues.push({
          window_id: window.id,
          dependency,
          problem: DependencyProblem::SelfDependency,
          severity: Severity::Error,
          detail: "the window depends on itself",
        })
        continue
      }
      match find_window(plan.windows, dependency) {
        None =>
          issues.push({
            window_id: window.id,
            dependency,
            problem: DependencyProblem::MissingDependency,
            severity: Severity::Error,
            detail: "no window with this id exists in the plan",
          })
        Some(target) =>
          if target.end > window.start {
            issues.push({
              window_id: window.id,
              dependency,
              problem: DependencyProblem::OutOfOrder,
              severity: Severity::Error,
              detail: "the dependency ends " +
              format_duration(target.end - window.start) +
              " after the dependent starts",
            })
          } else if target.end == window.start {
            issues.push({
              window_id: window.id,
              dependency,
              problem: DependencyProblem::BackToBack,
              severity: Severity::Warning,
              detail: "no slack between the dependency end and the dependent start",
            })
          }
      }
    }
  }
  {
    plan,
    overlaps,
    dependency_issues: issues,
    freeze_conflicts: freeze_conflicts(plan),
    blocked_minutes: blocked_minutes(plan),
  }
}

///|
/// Every pair of a planned change and a change freeze that share at least one
/// minute.
///
/// A freeze forbids changes, so each pair is a conflict. Pairs are produced in
/// freeze order, then in change order, which keeps the report reproducible.
pub fn freeze_conflicts(plan : Plan) -> Array[FreezeConflict] {
  let conflicts : Array[FreezeConflict] = []
  for freeze in plan.freezes() {
    for change in plan.changes() {
      let minutes = overlap_minutes(
        freeze.start,
        freeze.end,
        change.start,
        change.end,
      )
      if minutes > 0 {
        conflicts.push({ window_id: change.id, freeze_id: freeze.id, minutes, })
      }
    }
  }
  conflicts
}

///|
/// Freezes a candidate interval would run into, ordered by freeze start.
///
/// This is the freeze half of `can_schedule`; it is exported separately so that
/// callers can explain a refusal without running a full audit.
pub fn freezes_hit_by(plan : Plan, candidate : Slot) -> Array[Window] {
  let hits : Array[Window] = []
  for freeze in plan.freezes() {
    if overlap_minutes(freeze.start, freeze.end, candidate.start, candidate.end) >
      0 {
      hits.push(freeze)
    }
  }
  hits
}