use std::path::Path;
use crate::core::{self, TicketFile, ENV_VALUES, STATUS_VALUES};
#[derive(Debug)]
pub struct Repair {
pub file: String,
pub description: String,
pub tier: u8,
}
#[derive(Debug)]
pub struct Advisory {
pub file: String,
pub message: String,
pub suggestion: String,
}
pub struct FixResult {
pub repairs: Vec<Repair>,
pub advisories: Vec<Advisory>,
}
fn map_status(s: &str) -> Option<&'static str> {
match s {
"closed" => Some("done"),
"cancelled" | "canceled" => Some("done"),
_ => None,
}
}
pub fn run_fix(dir: &Path, dry_run: bool) -> anyhow::Result<FixResult> {
let mut repairs = Vec::new();
let mut advisories = Vec::new();
let mut entries: Vec<_> = std::fs::read_dir(dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in &entries {
let path = entry.path();
let fname = entry.file_name().to_string_lossy().to_string();
let file = match TicketFile::parse_str(&std::fs::read_to_string(&path)?, &path) {
Ok(f) => f,
Err(e) => {
let msg = e.to_string();
if msg.contains("missing required field: id")
|| msg.contains("missing required field: title")
{
advisories.push(Advisory {
file: fname,
message: "not a tkt ticket (missing required fields)".into(),
suggestion: "move to docs/ or remove from .tickets/".into(),
});
} else if msg.contains("missing required field") {
advisories.push(Advisory {
file: fname,
message: msg,
suggestion: "add the missing field manually".into(),
});
} else {
advisories.push(Advisory {
file: fname,
message: format!("parse error: {}", msg),
suggestion: "inspect file manually".into(),
});
}
continue;
}
};
let mut modified_file = file.clone();
let mut file_repairs: Vec<Repair> = Vec::new();
if let Some(raw_id) = modified_file.get("id") {
let trimmed = raw_id.trim().to_string();
if !trimmed.starts_with('"') {
let quoted = format!("\"{}\"", trimmed);
modified_file.set_field("id", "ed);
file_repairs.push(Repair {
file: fname.clone(),
description: format!("quoted id {} → {}", trimmed, quoted),
tier: 1,
});
}
}
if let Some(raw_deps) = modified_file.get("blocked_by") {
let trimmed = raw_deps.trim().to_string();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
let inner = &trimmed[1..trimmed.len() - 1];
if !inner.trim().is_empty() {
let elements: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
let needs_quoting = elements.iter().any(|e| !e.starts_with('"'));
if needs_quoting {
let quoted_elements: Vec<String> = elements
.iter()
.map(|e| {
let clean = e.trim_matches('"');
format!("\"{}\"", clean)
})
.collect();
let new_val = format!("[{}]", quoted_elements.join(", "));
modified_file.set_field("blocked_by", &new_val);
file_repairs.push(Repair {
file: fname.clone(),
description: "quoted blocked_by elements".to_string(),
tier: 1,
});
}
}
}
}
if let Some(raw_env) = modified_file.get("env") {
let val = raw_env.trim().trim_matches('"').to_string();
if !ENV_VALUES.contains(&val.as_str()) {
modified_file.remove_field("env");
file_repairs.push(Repair {
file: fname.clone(),
description: format!("removed invalid env: {}", val),
tier: 1,
});
}
}
if let Some(raw_prio) = modified_file.get("priority") {
let val = raw_prio.trim().trim_matches('"').to_string();
if core::Priority::parse(&val).is_none() {
modified_file.remove_field("priority");
file_repairs.push(Repair {
file: fname.clone(),
description: format!("removed invalid priority: {}", val),
tier: 1,
});
}
}
if let Some(raw_status) = modified_file.get("status") {
let val = raw_status.trim().trim_matches('"').to_string();
if !STATUS_VALUES.contains(&val.as_str()) {
if let Some(mapped) = map_status(&val) {
modified_file.set_field("status", mapped);
file_repairs.push(Repair {
file: fname.clone(),
description: format!("status: {} → {} (mapped)", val, mapped),
tier: 2,
});
} else {
advisories.push(Advisory {
file: fname.clone(),
message: format!("unknown status: {:?}", val),
suggestion:
"did you mean backlog or open? Run: tkt edit <id> --status backlog"
.to_string(),
});
}
}
}
if !file_repairs.is_empty() && !dry_run {
modified_file.write()?;
}
repairs.extend(file_repairs);
}
Ok(FixResult {
repairs,
advisories,
})
}