1use crate::errors::{CoreError, CoreResult};
7use crate::orchestrate::gates::default_gate_order;
8use crate::orchestrate::types::{
9 FailurePolicy, GatePolicy, OrchestrateRunConfig, parse_max_parallel,
10};
11use ito_domain::changes::ChangeOrchestrateMetadata;
12use serde::{Deserialize, Serialize};
13use std::collections::{BTreeMap, BTreeSet, VecDeque};
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct PlannedGate {
18 pub name: String,
20 pub policy: GatePolicy,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct PlannedChange {
27 pub id: String,
29 #[serde(default)]
31 pub depends_on: Vec<String>,
32 pub gates: Vec<PlannedGate>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37pub struct RunPlan {
39 pub run_id: String,
41 pub preset: String,
43 pub max_parallel: usize,
45 pub failure_policy: FailurePolicy,
47 pub changes: Vec<PlannedChange>,
49}
50
51#[derive(Debug, Clone)]
52pub struct ChangePlanInput {
54 pub id: String,
56 pub orchestrate: ChangeOrchestrateMetadata,
58}
59
60pub fn build_run_plan(
64 run_id: &str,
65 preset: &str,
66 config: OrchestrateRunConfig,
67 changes: Vec<ChangePlanInput>,
68) -> CoreResult<RunPlan> {
69 let max_parallel = parse_max_parallel(config.max_parallel, config.max_parallel_cap)?;
70 let failure_policy = config.failure_policy.unwrap_or(FailurePolicy::Remediate);
71
72 let change_ids = collect_change_ids(&changes);
73 let deps = build_dependency_map(&changes, &change_ids);
74
75 let ordered_ids = topo_sort(&deps)?;
76 let mut changes_by_id: BTreeMap<String, ChangePlanInput> = BTreeMap::new();
77 for c in changes {
78 changes_by_id.insert(c.id.clone(), c);
79 }
80
81 let default_order = if config.gate_order.is_empty() {
82 default_gate_order()
83 } else {
84 config.gate_order.clone()
85 };
86 let skip_gates = config.skip_gates;
87
88 let mut planned = Vec::new();
89 for id in ordered_ids {
90 let Some(input) = changes_by_id.remove(&id) else {
91 continue;
92 };
93
94 let gate_names = resolve_gate_names(&input, &default_order);
95 let gates = build_planned_gates(gate_names, &skip_gates);
96
97 let depends_on = deps
98 .get(&input.id)
99 .map(|s| s.iter().cloned().collect())
100 .unwrap_or_default();
101
102 planned.push(PlannedChange {
103 id: input.id,
104 depends_on,
105 gates,
106 });
107 }
108
109 Ok(RunPlan {
110 run_id: run_id.to_string(),
111 preset: preset.to_string(),
112 max_parallel,
113 failure_policy,
114 changes: planned,
115 })
116}
117
118fn collect_change_ids(changes: &[ChangePlanInput]) -> BTreeSet<String> {
119 let mut change_ids = BTreeSet::new();
120 for change in changes {
121 change_ids.insert(change.id.clone());
122 }
123 change_ids
124}
125
126fn build_dependency_map(
127 changes: &[ChangePlanInput],
128 change_ids: &BTreeSet<String>,
129) -> BTreeMap<String, BTreeSet<String>> {
130 let mut deps = BTreeMap::new();
131 for change in changes {
132 let depends_on = change
133 .orchestrate
134 .depends_on
135 .iter()
136 .filter(|dep| change_ids.contains(dep.as_str()))
137 .cloned()
138 .collect();
139 deps.insert(change.id.clone(), depends_on);
140 }
141 deps
142}
143
144fn resolve_gate_names(input: &ChangePlanInput, default_order: &[String]) -> Vec<String> {
145 if !input.orchestrate.preferred_gates.is_empty() {
146 return input.orchestrate.preferred_gates.clone();
147 }
148
149 default_order.to_vec()
150}
151
152fn build_planned_gates(gate_names: Vec<String>, skip_gates: &BTreeSet<String>) -> Vec<PlannedGate> {
153 gate_names
154 .into_iter()
155 .map(|name| PlannedGate {
156 policy: gate_policy_for_name(skip_gates, &name),
157 name,
158 })
159 .collect()
160}
161
162fn gate_policy_for_name(skip_gates: &BTreeSet<String>, gate_name: &str) -> GatePolicy {
163 if skip_gates.contains(gate_name) {
164 return GatePolicy::Skip;
165 }
166
167 GatePolicy::Run
168}
169
170fn topo_sort(deps: &BTreeMap<String, BTreeSet<String>>) -> CoreResult<Vec<String>> {
171 let mut indegree: BTreeMap<String, usize> = BTreeMap::new();
172 let mut reverse: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
173
174 for (node, node_deps) in deps {
175 indegree.insert(node.clone(), node_deps.len());
176 for dep in node_deps {
177 reverse.entry(dep.clone()).or_default().insert(node.clone());
178 }
179 }
180
181 let mut q: VecDeque<String> = VecDeque::new();
182 for (node, d) in &indegree {
183 if *d == 0 {
184 q.push_back(node.clone());
185 }
186 }
187
188 let mut out = Vec::new();
189 while let Some(n) = q.pop_front() {
190 out.push(n.clone());
191 let Some(children) = reverse.get(&n) else {
192 continue;
193 };
194 for child in children {
195 if let Some(d) = indegree.get_mut(child) {
196 *d = d.saturating_sub(1);
197 if *d == 0 {
198 q.push_back(child.clone());
199 }
200 }
201 }
202 }
203
204 if out.len() == deps.len() {
205 return Ok(out);
206 }
207
208 let cycle = find_cycle(deps).unwrap_or_else(|| vec!["<unknown>".to_string()]);
210 Err(CoreError::Validation(format!(
211 "circular depends_on cycle detected: {}",
212 cycle.join(" -> ")
213 )))
214}
215
216fn find_cycle(deps: &BTreeMap<String, BTreeSet<String>>) -> Option<Vec<String>> {
217 #[derive(Clone, Copy, PartialEq, Eq)]
218 enum Mark {
219 Temp,
220 Perm,
221 }
222
223 fn dfs(
224 node: &str,
225 deps: &BTreeMap<String, BTreeSet<String>>,
226 marks: &mut BTreeMap<String, Mark>,
227 stack: &mut Vec<String>,
228 ) -> Option<Vec<String>> {
229 match marks.get(node) {
230 Some(Mark::Temp) => {
231 return Some(build_cycle(stack, node));
232 }
233 Some(Mark::Perm) => {
234 return None;
235 }
236 None => {}
237 }
238
239 marks.insert(node.to_string(), Mark::Temp);
240 stack.push(node.to_string());
241 if let Some(next) = deps.get(node) {
242 for dep in next {
243 if let Some(cycle) = dfs(dep, deps, marks, stack) {
244 return Some(cycle);
245 }
246 }
247 }
248 stack.pop();
249 marks.insert(node.to_string(), Mark::Perm);
250 None
251 }
252
253 let mut marks: BTreeMap<String, Mark> = BTreeMap::new();
254 for node in deps.keys() {
255 let mut stack = Vec::new();
256 if let Some(cycle) = dfs(node, deps, &mut marks, &mut stack) {
257 return Some(cycle);
258 }
259 }
260 None
261}
262
263fn build_cycle(stack: &[String], node: &str) -> Vec<String> {
264 let start = stack
265 .iter()
266 .position(|candidate| candidate == node)
267 .unwrap_or(0);
268 let mut cycle = stack[start..].to_vec();
269 cycle.push(node.to_string());
270 cycle
271}