use time::OffsetDateTime;
use crate::entities::{CeremonyDefinition, CeremonyInstance};
use crate::error::DomainError;
use crate::value_objects::{MaxParallel, StateExecution, StepAttempt, StepId, StepStatus};
impl CeremonyInstance {
pub(super) fn resolved_step_is_claimable_at(
&self,
step_id: &StepId,
definition: &CeremonyDefinition,
now: OffsetDateTime,
host_ceiling: MaxParallel,
) -> Result<bool, DomainError> {
self.require_definition(definition)?;
let state = definition
.state(&self.current_state)
.ok_or(DomainError::NotFound {
what: "ceremony_instance.current_state",
})?;
let steps = definition
.steps_for_state(&self.current_state)
.collect::<Vec<_>>();
if state.execution() == StateExecution::Sequential {
let first = steps.into_iter().find(|step| {
self.step_records
.get(step.id())
.is_some_and(|record| !record.status().is_success())
});
if first.is_none_or(|step| step.id() != step_id) {
return Ok(false);
}
} else {
let capacity =
usize::from(definition.max_parallel().effective_with(host_ceiling).get());
let live = steps
.iter()
.filter(|step| {
self.step_records
.get(step.id())
.is_some_and(|record| record.has_live_lease_at(now))
})
.count();
if live >= capacity {
return Ok(false);
}
}
self.step_is_ready_without_role_at(step_id, definition, now)
}
pub fn claimable_step_ids_at<'a>(
&self,
definition: &'a CeremonyDefinition,
now: OffsetDateTime,
host_ceiling: MaxParallel,
) -> Result<Vec<&'a StepId>, DomainError> {
self.require_definition(definition)?;
let state = definition
.state(&self.current_state)
.ok_or(DomainError::NotFound {
what: "ceremony_instance.current_state",
})?;
let steps = definition
.steps_for_state(&self.current_state)
.collect::<Vec<_>>();
if state.execution() == StateExecution::Sequential {
let Some(step) = steps.into_iter().find(|step| {
self.step_records
.get(step.id())
.is_some_and(|record| !record.status().is_success())
}) else {
return Ok(Vec::new());
};
return Ok(if self.step_is_claimable_at(step.id(), definition, now)? {
vec![step.id()]
} else {
Vec::new()
});
}
let capacity = usize::from(definition.max_parallel().effective_with(host_ceiling).get());
let live = steps
.iter()
.filter(|step| {
self.step_records
.get(step.id())
.is_some_and(|record| record.has_live_lease_at(now))
})
.count();
if live >= capacity {
return Ok(Vec::new());
}
steps
.into_iter()
.filter_map(
|step| match self.step_is_claimable_at(step.id(), definition, now) {
Ok(true) => Some(Ok(step.id())),
Ok(false) => None,
Err(error) => Some(Err(error)),
},
)
.collect()
}
fn step_is_claimable_at(
&self,
step_id: &StepId,
definition: &CeremonyDefinition,
now: OffsetDateTime,
) -> Result<bool, DomainError> {
if self.resolve_step_role(definition, step_id, None).is_err() {
return Ok(false);
}
self.step_is_ready_without_role_at(step_id, definition, now)
}
fn step_is_ready_without_role_at(
&self,
step_id: &StepId,
definition: &CeremonyDefinition,
now: OffsetDateTime,
) -> Result<bool, DomainError> {
let step = definition.step(step_id).ok_or(DomainError::NotFound {
what: "ceremony_instance.step",
})?;
let record = self
.step_records
.get(step_id)
.ok_or(DomainError::NotFound {
what: "ceremony_instance.step_record",
})?;
if !record.can_be_started_at(now) {
return Ok(false);
}
let attempt = if matches!(record.status(), StepStatus::Failed | StepStatus::InProgress) {
record.attempt().next()?
} else {
StepAttempt::new(record.attempt().get())?
};
Ok(step.retry_policy().allows_attempt(attempt))
}
#[must_use]
pub fn has_live_step_leases_at(
&self,
definition: &CeremonyDefinition,
now: OffsetDateTime,
) -> bool {
definition.steps_for_state(&self.current_state).any(|step| {
self.step_records
.get(step.id())
.is_some_and(|record| record.has_live_lease_at(now))
})
}
}