///|
/// Operational analysis derived independently from the search algorithm.
pub(all) struct ShiftOperation {
  shift_id : ShiftId
  label : String
  group : String
  assigned : Int
  required : Int
  qualified : Int
  standby : Int
  coverage_percent : Int
  risk_score : Int
  risk_level : String
} derive(Debug, Eq)

///|
/// A transition between two adjacent duties performed by one person.
pub(all) struct Handover {
  person_id : PersonId
  from_shift : ShiftId
  to_shift : ShiftId
  gap_minutes : Int
  same_group : Bool
  risk_score : Int
} derive(Debug, Eq)

///|
/// Day-level summary for dashboards and post-run review.
pub(all) struct DayOperation {
  day : Int
  shift_count : Int
  required_slots : Int
  assigned_slots : Int
  uncovered_slots : Int
  people_used : Int
  scheduled_minutes : Int
  average_load : Int
  risk_score : Int
} derive(Debug, Eq)

///|
/// A concrete operational action generated from schedule evidence.
pub(all) struct OperationalAction {
  priority : Int
  category : String
  subject : String
  message : String
} derive(Debug, Eq)

///|
/// Full operational view; it remains useful even for a partial roster.
pub(all) struct OperationalReview {
  shifts : Array[ShiftOperation]
  days : Array[DayOperation]
  handovers : Array[Handover]
  actions : Array[OperationalAction]
  total_risk : Int
} derive(Debug, Eq)

///|
/// Count assignments attached to one shift.
fn operation_assignment_count(
  assignments : Array[Assignment],
  shift_id : ShiftId,
) -> Int {
  let mut count = 0
  for item in assignments {
    if item.shift_id == shift_id {
      count = count + 1
    }
  }
  count
}

///|
/// Count distinct people assigned inside a half-open time window.
fn distinct_people_in_window(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
  start : Int,
  end : Int,
) -> Int {
  let ids : Array[PersonId] = []
  for item in assignments {
    match find_shift(problem, item.shift_id) {
      Some(shift) =>
        if shift.time.start >= start && shift.time.start < end {
          let mut seen = false
          for id in ids {
            if id == item.person_id {
              seen = true
            }
          }
          if !seen {
            ids.push(item.person_id)
          }
        }
      None => ()
    }
  }
  ids.length()
}

///|
/// Determine whether a person is assigned to a shift.
fn is_assigned_to(
  assignments : Array[Assignment],
  person_id : PersonId,
  shift_id : ShiftId,
) -> Bool {
  for item in assignments {
    if item.person_id == person_id && item.shift_id == shift_id {
      return true
    }
  }
  false
}

///|
/// Count people who can serve as qualified standby for a shift.
fn standby_count(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
  shift : Shift,
) -> Int {
  let mut count = 0
  for person in problem.people {
    if !is_assigned_to(assignments, person.id, shift.id) &&
      is_available(person, shift) &&
      meets_requirements(person, shift) {
      count = count + 1
    }
  }
  count
}

///|
/// Count qualified and available people without depending on solver state.
fn qualified_count(problem : ScheduleProblem, shift : Shift) -> Int {
  let mut count = 0
  for person in problem.people {
    if is_available(person, shift) && meets_requirements(person, shift) {
      count = count + 1
    }
  }
  count
}

///|
/// Risk is intentionally transparent and bounded to 0..100.
fn shift_risk_score(
  required : Int,
  assigned : Int,
  qualified : Int,
  standby : Int,
  priority : Int,
) -> Int {
  let missing = if required > assigned { required - assigned } else { 0 }
  let shortage = if required > qualified { required - qualified } else { 0 }
  let mut score = missing * 35 + shortage * 20
  if assigned >= required && standby == 0 {
    score = score + 15
  } else if standby == 1 {
    score = score + 7
  }
  score = score + priority * missing * 3
  if score > 100 {
    100
  } else {
    score
  }
}

///|
/// Convert a numeric risk into a stable label for reports.
pub fn risk_level(score : Int) -> String {
  if score >= 70 {
    "critical"
  } else if score >= 40 {
    "high"
  } else if score >= 20 {
    "medium"
  } else {
    "low"
  }
}

///|
/// Analyze every shift, including uncovered ones.
pub fn analyze_shift_operations(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> Array[ShiftOperation] {
  let values : Array[ShiftOperation] = []
  for shift in problem.shifts {
    let assigned = operation_assignment_count(assignments, shift.id)
    let qualified = qualified_count(problem, shift)
    let standby = standby_count(problem, assignments, shift)
    let score = shift_risk_score(
      shift.required_people,
      assigned,
      qualified,
      standby,
      shift.priority,
    )
    values.push({
      shift_id: shift.id,
      label: shift.label,
      group: shift.group,
      assigned,
      required: shift.required_people,
      qualified,
      standby,
      coverage_percent: if shift.required_people == 0 {
        100
      } else {
        assigned * 100 / shift.required_people
      },
      risk_score: score,
      risk_level: risk_level(score),
    })
  }
  values.sort_by((a, b) => {
    if a.risk_score == b.risk_score {
      a.shift_id.compare(b.shift_id)
    } else {
      b.risk_score - a.risk_score
    }
  })
  values
}

///|
/// Extract assignment pairs for a person and order them chronologically.
fn person_duties(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
  person_id : PersonId,
) -> Array[Shift] {
  let duties : Array[Shift] = []
  for item in assignments {
    if item.person_id == person_id {
      match find_shift(problem, item.shift_id) {
        Some(shift) => duties.push(shift)
        None => ()
      }
    }
  }
  duties.sort_by((a, b) => a.time.start - b.time.start)
  duties
}

///|
/// Analyze every adjacent duty transition.
pub fn analyze_handovers(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> Array[Handover] {
  let values : Array[Handover] = []
  for person in problem.people {
    let duties = person_duties(problem, assignments, person.id)
    let mut index = 1
    while index < duties.length() {
      let previous = duties[index - 1]
      let next = duties[index]
      let gap = next.time.start - previous.time.end
      let same_group = previous.group == next.group
      let mut score = 0
      if gap < problem.minimum_rest_minutes {
        score = score + 60
      } else if gap < problem.minimum_rest_minutes * 2 {
        score = score + 20
      }
      if same_group {
        score = score + 5
      }
      if previous.time.end / 1440 != next.time.start / 1440 {
        score = score + 3
      }
      values.push({
        person_id: person.id,
        from_shift: previous.id,
        to_shift: next.id,
        gap_minutes: gap,
        same_group,
        risk_score: if score > 100 {
          100
        } else {
          score
        },
      })
      index = index + 1
    }
  }
  values.sort_by((a, b) => b.risk_score - a.risk_score)
  values
}

///|
/// Generate summaries only for days that contain at least one shift.
pub fn analyze_days(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> Array[DayOperation] {
  let days : Array[DayOperation] = []
  let mut day = 0
  let mut maximum_day = 0
  for shift in problem.shifts {
    let value = shift.time.start / 1440
    if value > maximum_day {
      maximum_day = value
    }
  }
  while day <= maximum_day {
    let start = day * 1440
    let end = start + 1440
    let mut shift_count = 0
    let mut required = 0
    let mut assigned = 0
    let mut minutes = 0
    let mut risk = 0
    for shift in problem.shifts {
      if shift.time.start >= start && shift.time.start < end {
        shift_count = shift_count + 1
        required = required + shift.required_people
        let actual = operation_assignment_count(assignments, shift.id)
        assigned = assigned + actual
        minutes = minutes + actual * shift.time.duration()
        risk = risk +
          shift_risk_score(
            shift.required_people,
            actual,
            qualified_count(problem, shift),
            standby_count(problem, assignments, shift),
            shift.priority,
          )
      }
    }
    if shift_count > 0 {
      let people = distinct_people_in_window(problem, assignments, start, end)
      days.push({
        day,
        shift_count,
        required_slots: required,
        assigned_slots: assigned,
        uncovered_slots: required - assigned,
        people_used: people,
        scheduled_minutes: minutes,
        average_load: if people == 0 {
          0
        } else {
          minutes / people
        },
        risk_score: if risk > 100 {
          100
        } else {
          risk
        },
      })
    }
    day = day + 1
  }
  days
}

///|
/// Turn evidence into a short, deterministic action queue.
pub fn build_operational_actions(
  shifts : Array[ShiftOperation],
  handovers : Array[Handover],
) -> Array[OperationalAction] {
  let actions : Array[OperationalAction] = []
  for item in shifts {
    if item.assigned < item.required {
      actions.push({
        priority: 100 - item.risk_score,
        category: "coverage",
        subject: item.shift_id,
        message: "Fill \{item.required - item.assigned} uncovered slot(s); \{item.qualified} qualified people are known.",
      })
    } else if item.standby == 0 {
      actions.push({
        priority: 70,
        category: "resilience",
        subject: item.shift_id,
        message: "Nominate a qualified standby because the shift has no replacement margin.",
      })
    }
  }
  for item in handovers {
    if item.risk_score >= 40 {
      actions.push({
        priority: 20,
        category: "rest",
        subject: item.person_id,
        message: "Review the \{item.gap_minutes}-minute gap between \{item.from_shift} and \{item.to_shift}.",
      })
    }
  }
  actions.sort_by((a, b) => {
    if a.priority == b.priority {
      a.subject.compare(b.subject)
    } else {
      a.priority - b.priority
    }
  })
  actions
}

///|
/// Build the full operational review in one call.
pub fn review_operations(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> OperationalReview {
  let shifts = analyze_shift_operations(problem, assignments)
  let handovers = analyze_handovers(problem, assignments)
  let days = analyze_days(problem, assignments)
  let actions = build_operational_actions(shifts, handovers)
  let mut total = 0
  for item in shifts {
    total = total + item.risk_score
  }
  for item in handovers {
    total = total + item.risk_score
  }
  { shifts, days, handovers, actions, total_risk: total }
}

///|
/// Render a compact Markdown operations section.
pub fn render_operations_markdown(review : OperationalReview) -> String {
  let mut text = "# Operational review\n\n"
  text = text + "- Aggregate risk: **\{review.total_risk}**\n"
  text = text + "- Recommended actions: **\{review.actions.length()}**\n\n"
  text = text + "## Shift resilience\n\n"
  text = text +
    "| Shift | Coverage | Standby | Risk | Level |\n|---|---:|---:|---:|---|\n"
  for item in review.shifts {
    text = text +
      "| \{item.label} | \{item.assigned}/\{item.required} | \{item.standby} | \{item.risk_score} | \{item.risk_level} |\n"
  }
  if review.actions.length() > 0 {
    text = text + "\n## Actions\n\n"
    for action in review.actions {
      text = text +
        "- **\{action.category}** `\{action.subject}`: \{action.message}\n"
    }
  }
  text
}