1use std::collections::BTreeMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use starweaver_core::Metadata;
9
10#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum TaskStatus {
14 #[default]
16 Pending,
17 InProgress,
19 Completed,
21}
22
23impl TaskStatus {
24 #[must_use]
26 pub fn parse(value: &str) -> Option<Self> {
27 match value.trim() {
28 "pending" => Some(Self::Pending),
29 "in_progress" => Some(Self::InProgress),
30 "completed" => Some(Self::Completed),
31 _ => None,
32 }
33 }
34
35 #[must_use]
37 pub const fn as_str(&self) -> &str {
38 match self {
39 Self::Pending => "pending",
40 Self::InProgress => "in_progress",
41 Self::Completed => "completed",
42 }
43 }
44
45 #[must_use]
47 pub const fn is_completed(&self) -> bool {
48 matches!(self, Self::Completed)
49 }
50}
51
52impl std::fmt::Display for TaskStatus {
53 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 formatter.write_str(self.as_str())
55 }
56}
57
58#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
60pub struct Task {
61 pub id: String,
63 pub subject: String,
65 pub description: String,
67 #[serde(default)]
69 pub status: TaskStatus,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub active_form: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub owner: Option<String>,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub blocked_by: Vec<String>,
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 pub blocks: Vec<String>,
82 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
84 pub metadata: Metadata,
85 #[serde(default = "Utc::now")]
87 pub created_at: DateTime<Utc>,
88 #[serde(default = "Utc::now")]
90 pub updated_at: DateTime<Utc>,
91}
92
93impl Task {
94 #[must_use]
96 pub fn new(
97 id: impl Into<String>,
98 subject: impl Into<String>,
99 description: impl Into<String>,
100 ) -> Self {
101 let now = Utc::now();
102 Self {
103 id: id.into(),
104 subject: subject.into(),
105 description: description.into(),
106 status: TaskStatus::Pending,
107 active_form: None,
108 owner: None,
109 blocked_by: Vec::new(),
110 blocks: Vec::new(),
111 metadata: Metadata::default(),
112 created_at: now,
113 updated_at: now,
114 }
115 }
116
117 #[must_use]
119 pub const fn is_blocked(&self) -> bool {
120 !self.blocked_by.is_empty()
121 }
122
123 #[must_use]
125 pub const fn status_str(&self) -> &str {
126 self.status.as_str()
127 }
128
129 pub fn set_status(&mut self, status: TaskStatus) {
131 self.status = status;
132 self.updated_at = Utc::now();
133 }
134}
135
136#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
138pub struct TaskSnapshot {
139 pub tasks: Vec<Task>,
141}
142
143impl TaskSnapshot {
144 #[must_use]
146 pub fn into_payload(self) -> Value {
147 serde_json::to_value(self).unwrap_or_else(|_| serde_json::json!({"tasks": []}))
148 }
149}
150
151#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
153pub struct TaskManager {
154 #[serde(default)]
156 pub tasks: BTreeMap<String, Task>,
157}
158
159impl TaskManager {
160 #[must_use]
162 pub fn new() -> Self {
163 Self::default()
164 }
165
166 pub fn create(
168 &mut self,
169 subject: impl Into<String>,
170 description: impl Into<String>,
171 active_form: Option<String>,
172 metadata: Metadata,
173 ) -> Task {
174 let id = self.next_id();
175 let mut task = Task::new(id.clone(), subject, description);
176 task.active_form = active_form;
177 task.metadata = metadata;
178 self.tasks.insert(id, task.clone());
179 task
180 }
181
182 #[must_use]
184 pub fn get(&self, task_id: &str) -> Option<&Task> {
185 self.tasks.get(task_id)
186 }
187
188 #[must_use]
190 pub fn list_all(&self) -> Vec<Task> {
191 let mut tasks = self.tasks.values().cloned().collect::<Vec<_>>();
192 tasks.sort_by_key(|task| task_sort_key(&task.id));
193 tasks
194 }
195
196 pub fn replace_all(&mut self, tasks: Vec<Task>) {
198 self.tasks = tasks
199 .into_iter()
200 .map(|task| (normalize_task_id(&task.id), task))
201 .collect();
202 }
203
204 #[allow(clippy::too_many_arguments)]
206 pub fn update(
207 &mut self,
208 task_id: &str,
209 status: Option<TaskStatus>,
210 subject: Option<String>,
211 description: Option<String>,
212 active_form: Option<Option<String>>,
213 owner: Option<Option<String>>,
214 add_blocks: Option<&[String]>,
215 add_blocked_by: Option<&[String]>,
216 metadata: Option<&Metadata>,
217 ) -> Option<Task> {
218 let task_id = normalize_task_id(task_id);
219 let was_completed = status.as_ref().is_some_and(TaskStatus::is_completed)
220 && self
221 .tasks
222 .get(&task_id)
223 .is_some_and(|task| !task.status.is_completed());
224 {
225 let task = self.tasks.get_mut(&task_id)?;
226 if let Some(status) = status {
227 task.status = status;
228 }
229 if let Some(subject) = subject.filter(|value| !value.trim().is_empty()) {
230 task.subject = subject;
231 }
232 if let Some(description) = description.filter(|value| !value.trim().is_empty()) {
233 task.description = description;
234 }
235 if let Some(active_form) = active_form {
236 task.active_form = active_form;
237 }
238 if let Some(owner) = owner {
239 task.owner = owner;
240 }
241 extend_unique(&mut task.blocks, add_blocks);
242 extend_unique(&mut task.blocked_by, add_blocked_by);
243 if let Some(metadata) = metadata {
244 for (key, value) in metadata {
245 task.metadata.insert(key.clone(), value.clone());
246 }
247 }
248 task.updated_at = Utc::now();
249 }
250
251 for blocked_id in add_blocks.unwrap_or_default() {
252 self.add_blocking_relationship(&task_id, blocked_id);
253 }
254 for blocker_id in add_blocked_by.unwrap_or_default() {
255 self.add_blocked_by_relationship(&task_id, blocker_id);
256 }
257 if was_completed {
258 self.resolve_completion(&task_id);
259 }
260 self.tasks.get(&task_id).cloned()
261 }
262
263 #[must_use]
265 pub fn export_tasks(&self) -> BTreeMap<String, Value> {
266 self.tasks
267 .iter()
268 .map(|(id, task)| {
269 (
270 id.clone(),
271 serde_json::to_value(task).unwrap_or_else(|_| serde_json::json!({})),
272 )
273 })
274 .collect()
275 }
276
277 #[must_use]
279 pub fn from_exported(data: BTreeMap<String, Value>) -> Self {
280 let tasks = data
281 .into_iter()
282 .filter_map(|(id, value)| {
283 serde_json::from_value::<Task>(value)
284 .ok()
285 .map(|task| (id, task))
286 })
287 .collect();
288 Self { tasks }
289 }
290
291 #[must_use]
293 pub fn is_empty(&self) -> bool {
294 self.tasks.is_empty()
295 }
296
297 fn next_id(&self) -> String {
298 self.tasks
299 .keys()
300 .filter_map(|id| normalize_task_id(id).parse::<u64>().ok())
301 .max()
302 .unwrap_or(0)
303 .saturating_add(1)
304 .to_string()
305 }
306
307 fn add_blocking_relationship(&mut self, task_id: &str, blocked_id: &str) {
308 let blocked_id = normalize_task_id(blocked_id);
309 if let Some(task) = self.tasks.get_mut(task_id) {
310 extend_unique(&mut task.blocks, Some(std::slice::from_ref(&blocked_id)));
311 }
312 if let Some(blocked_task) = self.tasks.get_mut(&blocked_id) {
313 extend_unique(
314 &mut blocked_task.blocked_by,
315 Some(std::slice::from_ref(&task_id.to_string())),
316 );
317 blocked_task.updated_at = Utc::now();
318 }
319 }
320
321 fn add_blocked_by_relationship(&mut self, task_id: &str, blocker_id: &str) {
322 let blocker_id = normalize_task_id(blocker_id);
323 if let Some(task) = self.tasks.get_mut(task_id) {
324 extend_unique(
325 &mut task.blocked_by,
326 Some(std::slice::from_ref(&blocker_id)),
327 );
328 }
329 if let Some(blocker) = self.tasks.get_mut(&blocker_id) {
330 extend_unique(
331 &mut blocker.blocks,
332 Some(std::slice::from_ref(&task_id.to_string())),
333 );
334 blocker.updated_at = Utc::now();
335 }
336 }
337
338 fn resolve_completion(&mut self, task_id: &str) {
339 let Some(blocked_ids) = self.tasks.get(task_id).map(|task| task.blocks.clone()) else {
340 return;
341 };
342 for blocked_id in blocked_ids {
343 if let Some(blocked_task) = self.tasks.get_mut(&blocked_id) {
344 blocked_task.blocked_by.retain(|blocker| blocker != task_id);
345 blocked_task.updated_at = Utc::now();
346 }
347 }
348 }
349}
350
351fn normalize_task_id(id: &str) -> String {
352 id.trim().trim_start_matches('#').to_string()
353}
354
355fn task_sort_key(id: &str) -> (u8, u64, String) {
356 normalize_task_id(id).parse::<u64>().map_or_else(
357 |_| (1, 0, id.to_string()),
358 |value| (0, value, String::new()),
359 )
360}
361
362fn extend_unique(target: &mut Vec<String>, values: Option<&[String]>) {
363 let Some(values) = values else {
364 return;
365 };
366 let mut seen = target
367 .iter()
368 .cloned()
369 .collect::<std::collections::BTreeSet<_>>();
370 for value in values {
371 let normalized = normalize_task_id(value);
372 if !normalized.is_empty() && seen.insert(normalized.clone()) {
373 target.push(normalized);
374 }
375 }
376}