use super::key::TaskEditKey;
use super::parsing::{
cycle_status, normalize_rfc3339_input, parse_list, parse_status, parse_task_agent_override,
};
use crate::contracts::{QueueFile, Task, TaskPriority, TaskStatus};
use crate::queue;
use crate::queue::ValidationWarning;
use crate::queue::operations::validate::{ensure_task_id, parse_custom_fields_with_context};
use anyhow::{Context, Result, anyhow, bail};
#[derive(Debug, Clone)]
pub struct TaskEditPreview {
pub task_id: String,
pub field: String,
pub old_value: String,
pub new_value: String,
pub warnings: Vec<ValidationWarning>,
}
#[allow(clippy::too_many_arguments)]
pub fn preview_task_edit(
queue: &QueueFile,
done: Option<&QueueFile>,
task_id: &str,
key: TaskEditKey,
input: &str,
now_rfc3339: &str,
id_prefix: &str,
id_width: usize,
max_dependency_depth: u8,
) -> Result<TaskEditPreview> {
let operation = "preview edit";
let needle = ensure_task_id(task_id, operation)?;
let task = queue
.tasks
.iter()
.find(|t| t.id.trim() == needle)
.ok_or_else(|| {
anyhow!(
"{}",
crate::error_messages::task_not_found_for_edit("edit preview", needle)
)
})?;
let mut preview_task = task.clone();
let trimmed = input.trim();
match key {
TaskEditKey::Title => {
if trimmed.is_empty() {
bail!(
"Queue edit preview failed (task_id={}, field=title): title cannot be empty. Provide a non-empty title (e.g., 'Fix login bug').",
needle
);
}
preview_task.title = trimmed.to_string();
}
TaskEditKey::Description => {
preview_task.description = if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
};
}
TaskEditKey::Status => {
let next_status = if trimmed.is_empty() {
cycle_status(preview_task.status)
} else {
parse_status(trimmed).with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=status)",
needle
)
})?
};
preview_task.status = next_status;
if next_status == TaskStatus::Done || next_status == TaskStatus::Rejected {
preview_task.completed_at = Some(now_rfc3339.to_string());
} else {
preview_task.completed_at = None;
}
}
TaskEditKey::Priority => {
preview_task.priority = if trimmed.is_empty() {
preview_task.priority.cycle()
} else {
trimmed.parse::<TaskPriority>().with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=priority)",
needle
)
})?
};
}
TaskEditKey::Tags => {
preview_task.tags = parse_list(trimmed);
}
TaskEditKey::Scope => {
preview_task.scope = parse_list(trimmed);
}
TaskEditKey::Evidence => {
preview_task.evidence = parse_list(trimmed);
}
TaskEditKey::Plan => {
preview_task.plan = parse_list(trimmed);
}
TaskEditKey::Notes => {
preview_task.notes = parse_list(trimmed);
}
TaskEditKey::Request => {
preview_task.request = if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
};
}
TaskEditKey::DependsOn => {
preview_task.depends_on = parse_list(trimmed);
}
TaskEditKey::Blocks => {
preview_task.blocks = parse_list(trimmed);
}
TaskEditKey::RelatesTo => {
preview_task.relates_to = parse_list(trimmed);
}
TaskEditKey::Duplicates => {
preview_task.duplicates = if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
};
}
TaskEditKey::CustomFields => {
preview_task.custom_fields =
parse_custom_fields_with_context(needle, trimmed, operation)?;
}
TaskEditKey::Agent => {
preview_task.agent = parse_task_agent_override(trimmed).with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=agent)",
needle
)
})?;
}
TaskEditKey::CreatedAt => {
let normalized = normalize_rfc3339_input("created_at", trimmed).with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=created_at)",
needle
)
})?;
preview_task.created_at = normalized;
}
TaskEditKey::UpdatedAt => {
let normalized = normalize_rfc3339_input("updated_at", trimmed).with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=updated_at)",
needle
)
})?;
preview_task.updated_at = normalized;
}
TaskEditKey::CompletedAt => {
let normalized =
normalize_rfc3339_input("completed_at", trimmed).with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=completed_at)",
needle
)
})?;
preview_task.completed_at = normalized;
}
TaskEditKey::StartedAt => {
let normalized = normalize_rfc3339_input("started_at", trimmed).with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=started_at)",
needle
)
})?;
preview_task.started_at = normalized;
}
TaskEditKey::ScheduledStart => {
let normalized =
normalize_rfc3339_input("scheduled_start", trimmed).with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=scheduled_start)",
needle
)
})?;
preview_task.scheduled_start = normalized;
}
TaskEditKey::EstimatedMinutes => {
let minutes = if trimmed.is_empty() {
None
} else {
Some(trimmed.parse::<u32>().with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=estimated_minutes): must be a non-negative integer",
needle
)
})?)
};
preview_task.estimated_minutes = minutes;
}
TaskEditKey::ActualMinutes => {
let minutes = if trimmed.is_empty() {
None
} else {
Some(trimmed.parse::<u32>().with_context(|| {
format!(
"Queue edit preview failed (task_id={}, field=actual_minutes): must be a non-negative integer",
needle
)
})?)
};
preview_task.actual_minutes = minutes;
}
}
if !matches!(key, TaskEditKey::UpdatedAt) {
preview_task.updated_at = Some(now_rfc3339.to_string());
}
let mut preview_queue = queue.clone();
if let Some(index) = preview_queue
.tasks
.iter()
.position(|t| t.id.trim() == needle)
{
preview_queue.tasks[index] = preview_task.clone();
}
let warnings = match queue::validate_queue_set(
&preview_queue,
done,
id_prefix,
id_width,
max_dependency_depth,
) {
Ok(warnings) => warnings,
Err(err) => {
bail!(
"Queue edit preview failed (task_id={}): validation error - {}",
needle,
err
);
}
};
let old_value = format_field_value(task, key);
let new_value = format_field_value(&preview_task, key);
Ok(TaskEditPreview {
task_id: needle.to_string(),
field: key.as_str().to_string(),
old_value,
new_value,
warnings,
})
}
pub(crate) fn format_field_value(task: &Task, key: TaskEditKey) -> String {
let sep = match key {
TaskEditKey::Evidence | TaskEditKey::Plan | TaskEditKey::Notes => "; ",
_ => ", ",
};
key.format_value(task, sep)
}