use crate::db::repository::PlanRepository;
use crate::services::ServiceContext;
use crate::tui::plan::{PlanDocument, PlanStatus, TaskStatus};
use anyhow::Result;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct PlanValidationWarning {
pub severity: WarningSeverity,
pub message: String,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WarningSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct PlanStatistics {
pub total_plans: usize,
pub completed_plans: usize,
pub in_progress_plans: usize,
pub average_tasks_per_plan: f64,
pub average_completion_rate: f64,
pub total_tasks_executed: usize,
pub total_tasks_succeeded: usize,
pub total_tasks_failed: usize,
}
#[derive(Clone)]
pub struct PlanService {
repo: PlanRepository,
}
impl PlanService {
pub fn new(context: ServiceContext) -> Self {
Self {
repo: PlanRepository::new(context.pool()),
}
}
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<PlanDocument>> {
self.repo.find_by_id(id).await
}
pub async fn find_by_session_id(&self, session_id: Uuid) -> Result<Vec<PlanDocument>> {
self.repo.find_by_session_id(session_id).await
}
pub async fn get_most_recent_plan(&self, session_id: Uuid) -> Result<Option<PlanDocument>> {
let plans = self.find_by_session_id(session_id).await?;
Ok(plans.into_iter().next())
}
pub async fn create(&self, plan: &PlanDocument) -> Result<()> {
self.repo.create(plan).await
}
pub async fn update(&self, plan: &PlanDocument) -> Result<()> {
self.repo.update(plan).await
}
pub async fn delete(&self, id: Uuid) -> Result<()> {
self.repo.delete(id).await
}
pub async fn export_to_json(
&self,
plan: &PlanDocument,
file_path: &std::path::Path,
) -> Result<()> {
let json = serde_json::to_string_pretty(plan)?;
let temp_file = file_path.with_extension("tmp");
tokio::fs::write(&temp_file, &json).await?;
tokio::fs::rename(&temp_file, file_path).await?;
Ok(())
}
pub async fn import_from_json(&self, file_path: &std::path::Path) -> Result<PlanDocument> {
let content = tokio::fs::read_to_string(file_path).await?;
let plan: PlanDocument = serde_json::from_str(&content)?;
Ok(plan)
}
pub fn validate_plan(&self, plan: &PlanDocument) -> Vec<PlanValidationWarning> {
let mut warnings = Vec::new();
for task in &plan.tasks {
if task.complexity >= 5 {
warnings.push(PlanValidationWarning {
severity: WarningSeverity::Warning,
message: format!(
"Task '{}' has maximum complexity ({}★)",
task.title, task.complexity
),
suggestion: Some(
"Consider breaking this task into smaller, more manageable subtasks."
.to_string(),
),
});
}
if task.description.len() < 50 {
warnings.push(PlanValidationWarning {
severity: WarningSeverity::Info,
message: format!(
"Task '{}' has a brief description ({} chars)",
task.title,
task.description.len()
),
suggestion: Some(
"Add more detailed implementation steps for better clarity.".to_string(),
),
});
}
if task.acceptance_criteria.is_empty() {
warnings.push(PlanValidationWarning {
severity: WarningSeverity::Info,
message: format!("Task '{}' has no acceptance criteria", task.title),
suggestion: Some(
"Define clear success criteria to know when the task is complete."
.to_string(),
),
});
}
}
if plan.tasks.len() > 20 {
warnings.push(PlanValidationWarning {
severity: WarningSeverity::Warning,
message: format!("Plan has {} tasks (>20)", plan.tasks.len()),
suggestion: Some(
"Consider splitting into multiple smaller plans for better manageability."
.to_string(),
),
});
}
if plan.tasks.is_empty() {
warnings.push(PlanValidationWarning {
severity: WarningSeverity::Error,
message: "Plan has no tasks".to_string(),
suggestion: Some("Add at least one task before finalizing the plan.".to_string()),
});
}
if plan.context.is_empty() {
warnings.push(PlanValidationWarning {
severity: WarningSeverity::Info,
message: "Plan has no context information".to_string(),
suggestion: Some(
"Add context about the environment, constraints, or assumptions.".to_string(),
),
});
}
if plan.risks.is_empty() {
warnings.push(PlanValidationWarning {
severity: WarningSeverity::Info,
message: "Plan has no identified risks".to_string(),
suggestion: Some(
"Consider what could go wrong and document potential risks.".to_string(),
),
});
}
warnings
}
pub async fn get_plan_history(&self, session_id: Uuid) -> Result<Vec<PlanDocument>> {
self.find_by_session_id(session_id).await
}
pub async fn get_completed_plans(&self, session_id: Uuid) -> Result<Vec<PlanDocument>> {
let plans = self.find_by_session_id(session_id).await?;
Ok(plans
.into_iter()
.filter(|p| p.status == PlanStatus::Completed)
.collect())
}
pub async fn get_active_plans(&self, session_id: Uuid) -> Result<Vec<PlanDocument>> {
let plans = self.find_by_session_id(session_id).await?;
Ok(plans
.into_iter()
.filter(|p| {
matches!(
p.status,
PlanStatus::InProgress | PlanStatus::PendingApproval | PlanStatus::Approved
)
})
.collect())
}
pub async fn get_statistics(&self, session_id: Uuid) -> Result<PlanStatistics> {
let plans = self.find_by_session_id(session_id).await?;
let total_plans = plans.len();
let completed_plans = plans
.iter()
.filter(|p| p.status == PlanStatus::Completed)
.count();
let in_progress_plans = plans
.iter()
.filter(|p| p.status == PlanStatus::InProgress)
.count();
let total_tasks: usize = plans.iter().map(|p| p.tasks.len()).sum();
let average_tasks_per_plan = if total_plans > 0 {
total_tasks as f64 / total_plans as f64
} else {
0.0
};
let mut total_completion_rate = 0.0;
let mut plans_with_tasks = 0;
let mut total_tasks_succeeded = 0;
let mut total_tasks_failed = 0;
for plan in &plans {
if !plan.tasks.is_empty() {
total_completion_rate += plan.progress_percentage() as f64;
plans_with_tasks += 1;
}
for task in &plan.tasks {
match task.status {
TaskStatus::Completed => total_tasks_succeeded += 1,
TaskStatus::Failed => total_tasks_failed += 1,
_ => {}
}
}
}
let average_completion_rate = if plans_with_tasks > 0 {
total_completion_rate / plans_with_tasks as f64
} else {
0.0
};
Ok(PlanStatistics {
total_plans,
completed_plans,
in_progress_plans,
average_tasks_per_plan,
average_completion_rate,
total_tasks_executed: total_tasks_succeeded + total_tasks_failed,
total_tasks_succeeded,
total_tasks_failed,
})
}
}