///|
/// Lifecycle states for one recovery step.
pub(all) enum StepStatus {
  Pending
  AwaitingApproval
  InProgress
  Succeeded
}

///|
/// Lifecycle states for a recovery playbook.
pub(all) enum PlaybookStatus {
  Draft
  Active
  Completed
}

///|
/// A human-owned recovery action and its step prerequisites.
pub(all) struct RecoveryStep {
  id : String
  asset : AssetRef
  owner : String
  requires_approval : Bool
  prerequisites : Array[String]
  status : StepStatus
}

///|
/// Creates a pending recovery step.
pub fn RecoveryStep::new(
  id~ : String,
  asset~ : AssetRef,
  owner~ : String,
  requires_approval~ : Bool,
  prerequisites~ : Array[String],
) -> RecoveryStep raise ModelError {
  if id == "" {
    raise ModelError::EmptyField("playbook.step_id")
  }
  if owner == "" {
    raise ModelError::EmptyField("playbook.owner")
  }
  { id, asset, owner, requires_approval, prerequisites, status: Pending, }
}

///|
fn RecoveryStep::with_status(
  self : RecoveryStep,
  status : StepStatus,
) -> RecoveryStep {
  {
    id: self.id,
    asset: self.asset,
    owner: self.owner,
    requires_approval: self.requires_approval,
    prerequisites: self.prerequisites,
    status,
  }
}

///|
/// A validated collection of recovery steps with a lifecycle status.
pub(all) struct RecoveryPlaybook {
  id : String
  steps : Array[RecoveryStep]
  status : PlaybookStatus
}

///|
fn step_index(steps : Array[RecoveryStep], id : String) -> Int? {
  let mut result : Int? = None
  for i, step in steps {
    if result is None && step.id == id {
      result = Some(i)
    }
  }
  result
}

///|
fn validate_step_references(
  steps : Array[RecoveryStep],
) -> Unit raise ModelError {
  for step in steps {
    for prerequisite_index, prerequisite in step.prerequisites {
      if prerequisite == step.id {
        raise ModelError::PlaybookValidation(
          "step \{step.id} cannot depend on itself",
        )
      }
      if step_index(steps, prerequisite) is None {
        raise ModelError::PlaybookValidation(
          "missing prerequisite \{prerequisite} for step \{step.id}",
        )
      }
      for earlier_index in 0.. RecoveryPlaybook raise ModelError {
  if id == "" {
    raise ModelError::EmptyField("playbook.id")
  }
  if steps.length() == 0 {
    raise ModelError::EmptyField("playbook.steps")
  }
  for i in 0.. Array[RecoveryStep] {
  let updated : Array[RecoveryStep] = []
  for i, step in steps {
    if i == index {
      updated.push(step.with_status(status))
    } else {
      updated.push(step)
    }
  }
  updated
}

///|
fn first_unfinished_prerequisite(
  step : RecoveryStep,
  steps : Array[RecoveryStep],
) -> String? {
  let mut blocker : String? = None
  for prerequisite in step.prerequisites {
    match step_index(steps, prerequisite) {
      Some(index) =>
        match steps[index].status {
          Succeeded => ()
          _ => if blocker is None { blocker = Some(prerequisite) }
        }
      None => if blocker is None { blocker = Some(prerequisite) }
    }
  }
  blocker
}

///|
fn status_after_update(steps : Array[RecoveryStep]) -> PlaybookStatus {
  let mut all_succeeded = true
  for step in steps {
    match step.status {
      Succeeded => ()
      _ => all_succeeded = false
    }
  }
  if all_succeeded {
    Completed
  } else {
    Active
  }
}

///|
/// Moves a step to execution or to its approval gate.
pub fn RecoveryPlaybook::begin_step(
  self : RecoveryPlaybook,
  step_id~ : String,
) -> RecoveryPlaybook raise ModelError {
  let index = match step_index(self.steps, step_id) {
    Some(index) => index
    None => raise ModelError::StepNotFound(step_id)
  }
  let step = self.steps[index]
  match step.status {
    Pending =>
      match first_unfinished_prerequisite(step, self.steps) {
        Some(prerequisite) =>
          raise ModelError::StepBlocked(
            "step \{step.id} waits for prerequisite \{prerequisite}",
          )
        None => {
          let next_status = if step.requires_approval {
            AwaitingApproval
          } else {
            InProgress
          }
          let updated = replace_step_status(self.steps, index, next_status)
          { id: self.id, steps: updated, status: Active, }
        }
      }
    AwaitingApproval =>
      raise ModelError::InvalidStepTransition(
        "step \{step.id} is awaiting approval",
      )
    InProgress =>
      raise ModelError::InvalidStepTransition(
        "step \{step.id} is already in progress",
      )
    Succeeded =>
      raise ModelError::InvalidStepTransition(
        "step \{step.id} has already succeeded",
      )
  }
}

///|
/// Approves a step that is waiting at its manual approval gate.
pub fn RecoveryPlaybook::approve_step(
  self : RecoveryPlaybook,
  step_id~ : String,
) -> RecoveryPlaybook raise ModelError {
  let index = match step_index(self.steps, step_id) {
    Some(index) => index
    None => raise ModelError::StepNotFound(step_id)
  }
  let step = self.steps[index]
  match step.status {
    AwaitingApproval => {
      let updated = replace_step_status(self.steps, index, InProgress)
      { id: self.id, steps: updated, status: Active, }
    }
    _ =>
      raise ModelError::InvalidStepTransition(
        "step \{step.id} is not awaiting approval",
      )
  }
}

///|
/// Marks an in-progress step as successfully completed.
pub fn RecoveryPlaybook::complete_step(
  self : RecoveryPlaybook,
  step_id~ : String,
) -> RecoveryPlaybook raise ModelError {
  let index = match step_index(self.steps, step_id) {
    Some(index) => index
    None => raise ModelError::StepNotFound(step_id)
  }
  let step = self.steps[index]
  match step.status {
    InProgress => {
      let updated = replace_step_status(self.steps, index, Succeeded)
      { id: self.id, steps: updated, status: status_after_update(updated), }
    }
    _ =>
      raise ModelError::InvalidStepTransition(
        "step \{step.id} is not in progress",
      )
  }
}