use crate::db::models::{Project, Session};
use crate::db::repository::ProjectRepository;
use crate::services::ServiceContext;
use anyhow::{Context, Result};
use chrono::Utc;
use uuid::Uuid;
#[derive(Clone)]
pub struct ProjectService {
context: ServiceContext,
}
#[derive(Debug, Clone)]
pub struct ProjectStats {
pub session_count: i64,
pub file_count: i64,
}
impl ProjectService {
pub fn new(context: ServiceContext) -> Self {
Self { context }
}
pub fn projects_dir() -> std::path::PathBuf {
crate::config::opencrabs_home().join("projects")
}
pub fn ensure_projects_dir() -> Result<std::path::PathBuf> {
let dir = Self::projects_dir();
if !dir.exists() {
std::fs::create_dir_all(&dir).with_context(|| {
format!("Failed to create projects directory: {}", dir.display())
})?;
tracing::info!("Created projects directory: {}", dir.display());
}
Ok(dir)
}
pub async fn project_brain_dir(
&self,
session_id: Uuid,
) -> Option<(String, std::path::PathBuf)> {
use crate::db::repository::SessionRepository;
let session = SessionRepository::new(self.context.pool())
.find_by_id(session_id)
.await
.ok()??;
let project_id = session.project_id?;
let project = ProjectRepository::new(self.context.pool())
.find_by_id(project_id)
.await
.ok()??;
let dir =
Self::projects_dir().join(crate::services::file::slugify_project_name(&project.name));
Some((project.name, dir))
}
pub async fn create_project(
&self,
name: String,
description: Option<String>,
) -> Result<Project> {
Self::ensure_projects_dir()?;
let repo = ProjectRepository::new(self.context.pool());
if let Some(existing) = repo.find_by_name(&name).await? {
anyhow::bail!(
"Project '{}' already exists (id: {})",
existing.name,
existing.id
);
}
let project = Project::new(name, description);
repo.create(&project).await?;
Ok(project)
}
pub async fn get_project(&self, id: Uuid) -> Result<Option<Project>> {
let repo = ProjectRepository::new(self.context.pool());
repo.find_by_id(id).await
}
pub async fn get_project_required(&self, id: Uuid) -> Result<Project> {
self.get_project(id)
.await?
.ok_or_else(|| anyhow::anyhow!("Project not found: {}", id))
}
pub async fn list_projects(&self) -> Result<Vec<Project>> {
let repo = ProjectRepository::new(self.context.pool());
repo.list_all().await
}
pub async fn update_project(&self, project: &Project) -> Result<()> {
let mut updated = project.clone();
updated.updated_at = Utc::now();
let repo = ProjectRepository::new(self.context.pool());
repo.update(&updated).await
}
pub async fn rename_project(&self, id: Uuid, new_name: String) -> Result<Project> {
let mut project = self.get_project_required(id).await?;
project.name = new_name;
project.updated_at = Utc::now();
let repo = ProjectRepository::new(self.context.pool());
repo.update(&project).await?;
Ok(project)
}
pub async fn delete_project(&self, id: Uuid) -> Result<()> {
let repo = ProjectRepository::new(self.context.pool());
repo.delete(id).await
}
pub async fn assign_session(&self, session_id: Uuid, project_id: Uuid) -> Result<()> {
self.get_project_required(project_id).await?;
let repo = ProjectRepository::new(self.context.pool());
repo.assign_session(session_id, project_id).await
}
pub async fn unassign_session(&self, session_id: Uuid) -> Result<()> {
let repo = ProjectRepository::new(self.context.pool());
repo.unassign_session(session_id).await
}
pub async fn get_sessions_for_project(&self, project_id: Uuid) -> Result<Vec<Session>> {
let repo = ProjectRepository::new(self.context.pool());
repo.find_sessions_by_project(project_id).await
}
pub async fn get_unassigned_sessions(&self) -> Result<Vec<Session>> {
let repo = ProjectRepository::new(self.context.pool());
repo.find_unassigned_sessions().await
}
pub async fn get_project_stats(&self, project_id: Uuid) -> Result<ProjectStats> {
let repo = ProjectRepository::new(self.context.pool());
let session_count = repo.count_sessions(project_id).await?;
let file_count = repo.count_files(project_id).await?;
Ok(ProjectStats {
session_count,
file_count,
})
}
}