use crate::commands::cloudflare_config::is_interactive_terminal;
use crate::config::TodosConfig;
use crate::utils::find_xbp_config_upwards;
use colored::Colorize;
#[cfg(feature = "linear")]
use dialoguer::Input;
use dialoguer::{theme::ColorfulTheme, Confirm, Select};
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::PathBuf;
pub async fn run_setup(namespace: &str) -> Result<(), String> {
let config_key = if namespace == "issues" {
"issues"
} else {
"todos"
};
if !is_interactive_terminal() {
return Err(format!(
"Interactive setup requires a TTY. Write an `{config_key}:` block to `.xbp/xbp.yaml` manually."
));
}
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!("{}", "Issue 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())?;
let openrouter_enrich = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(
"Opt-in: enrich issue titles/bodies with OpenRouter (`openrouter_enrich`)? Default no.",
)
.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();
#[cfg(feature = "linear")]
let project: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Linear default project name/id (empty to skip)")
.allow_empty(true)
.interact_text()
.map_err(|e| e.to_string())?;
#[cfg(not(feature = "linear"))]
let project = String::new();
let watch_raw: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Watch paths (comma-separated prefixes; empty = whole tree)")
.allow_empty(true)
.with_initial_text("crates/cli,crates/core,crates/mcp")
.interact_text()
.map_err(|e| e.to_string())?;
let watch_paths: Vec<String> = watch_raw
.split(',')
.map(|s| s.trim().replace('\\', "/").trim_matches('/').to_string())
.filter(|s| !s.is_empty())
.collect();
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())
},
linear_project: if project.trim().is_empty() {
None
} else {
Some(project.trim().to_string())
},
watch_paths: if watch_paths.is_empty() {
None
} else {
Some(watch_paths)
},
linear_link_wait_secs: Some(20),
linear_link_poll_ms: Some(2000),
purge_duplicate_linear: Some(true),
kinds: None,
priority_by_kind: Some(priority),
annotate_source: Some(annotate),
openrouter_enrich: Some(openrouter_enrich),
openrouter_enrich_model: None,
};
write_project_todos_config(
&found.config_path,
found.kind,
config_key,
&todos,
team.trim(),
)?;
println!();
println!(
"{} Wrote issue automation settings to {}",
"OK".bright_green().bold(),
found.config_path.display()
);
println!(
"{}",
format!("Try: `xbp {namespace} scan` then `xbp {namespace} sync --dry-run`").dimmed()
);
Ok(())
}
fn write_project_todos_config(
config_path: &PathBuf,
kind: &str,
config_key: &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(config_key.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(())
}