use crate::commands::cloudflare_config::is_interactive_terminal;
use crate::config::TodosConfig;
use crate::utils::find_xbp_config_upwards;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Confirm, Select};
#[cfg(feature = "linear")]
use dialoguer::Input;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::PathBuf;
pub async fn run_setup() -> Result<(), String> {
if !is_interactive_terminal() {
return Err(
"Interactive setup requires a TTY. Write a `todos:` block to `.xbp/xbp.yaml` manually."
.into(),
);
}
let cwd = env::current_dir().map_err(|e| format!("cwd: {e}"))?;
let found = find_xbp_config_upwards(&cwd).ok_or_else(|| {
"No XBP project config found. Run `xbp init` or create `.xbp/xbp.yaml` first.".to_string()
})?;
println!();
println!("{}", "TODO automation setup".bright_cyan().bold());
println!("Config: {}", found.config_path.display());
#[cfg(feature = "linear")]
let targets: &[&str] = &["both", "linear", "github"];
#[cfg(not(feature = "linear"))]
let targets: &[&str] = &["github"];
let to_idx = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Default sync target")
.items(targets)
.default(0)
.interact()
.map_err(|e| e.to_string())?;
let auto_yes = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Auto-file all TODOs without multi-select (`auto_yes`)?")
.default(false)
.interact()
.map_err(|e| e.to_string())?;
let prompt_sync = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("After scan, offer to sync interactively?")
.default(true)
.interact()
.map_err(|e| e.to_string())?;
let annotate = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Stamp source TODO lines with issue IDs after create (`annotate_source`)?")
.default(false)
.interact()
.map_err(|e| e.to_string())?;
#[cfg(feature = "linear")]
let team: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Linear default team key (empty to skip)")
.allow_empty(true)
.interact_text()
.map_err(|e| e.to_string())?;
#[cfg(not(feature = "linear"))]
let team = String::new();
#[cfg(feature = "linear")]
let assignee: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Linear default assignee (`me` / empty to skip)")
.allow_empty(true)
.with_initial_text("me")
.interact_text()
.map_err(|e| e.to_string())?;
#[cfg(not(feature = "linear"))]
let assignee = String::new();
let mut priority = BTreeMap::new();
priority.insert("FIXME".into(), 1);
priority.insert("HACK".into(), 2);
priority.insert("TODO".into(), 3);
priority.insert("XXX".into(), 4);
let todos = TodosConfig {
default_to: Some(targets[to_idx].to_string()),
auto_yes: Some(auto_yes),
prompt_sync_after_scan: Some(prompt_sync),
linear_labels: Some(vec!["xbp-todo".into()]),
github_labels: Some(vec!["xbp-todo".into()]),
linear_assignee: if assignee.trim().is_empty() {
None
} else {
Some(assignee.trim().to_string())
},
kinds: None,
priority_by_kind: Some(priority),
annotate_source: Some(annotate),
};
write_project_todos_config(&found.config_path, found.kind, &todos, team.trim())?;
println!();
println!(
"{} Wrote todos automation settings to {}",
"OK".bright_green().bold(),
found.config_path.display()
);
println!(
"{}",
"Try: `xbp todos scan` then `xbp todos sync --dry-run`".dimmed()
);
Ok(())
}
fn write_project_todos_config(
config_path: &PathBuf,
kind: &str,
todos: &TodosConfig,
team_key: &str,
) -> Result<(), String> {
let content = fs::read_to_string(config_path)
.map_err(|e| format!("Failed to read {}: {e}", config_path.display()))?;
let mut root: Value = if kind == "yaml" {
let yaml: serde_yaml::Value = serde_yaml::from_str(&content)
.map_err(|e| format!("Failed to parse YAML: {e}"))?;
serde_json::to_value(yaml).map_err(|e| e.to_string())?
} else {
serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {e}"))?
};
let obj = root
.as_object_mut()
.ok_or_else(|| "Project config root must be a mapping/object.".to_string())?;
let todos_value = serde_json::to_value(todos).map_err(|e| e.to_string())?;
obj.insert("todos".into(), todos_value);
if !team_key.is_empty() {
let linear = obj.entry("linear".to_string()).or_insert_with(|| json!({}));
if let Some(map) = linear.as_object_mut() {
map.insert("default_team_key".into(), json!(team_key));
} else {
*linear = json!({ "default_team_key": team_key });
}
}
if kind == "yaml" {
let yaml_val: serde_yaml::Value =
serde_json::from_value(root).map_err(|e| e.to_string())?;
let yaml = serde_yaml::to_string(&yaml_val).map_err(|e| e.to_string())?;
fs::write(config_path, yaml)
.map_err(|e| format!("Failed to write {}: {e}", config_path.display()))?;
} else {
let json = serde_json::to_string_pretty(&root).map_err(|e| e.to_string())?;
fs::write(config_path, json + "\n")
.map_err(|e| format!("Failed to write {}: {e}", config_path.display()))?;
}
Ok(())
}