use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use chrono::Utc;
use parking_lot::RwLock;
use super::{detect_project, DetectionResult, Project, ProjectId, ProjectSource};
use crate::event_bus::{EventBus, KernelEvent};
use crate::memory::database::MemoryDatabase;
#[derive(thiserror::Error, Debug)]
pub enum ProjectManagerError {
#[error("Project not found: {0}")]
NotFound(ProjectId),
#[error("Project name already exists: {0}")]
DuplicateName(String),
#[error("Invalid operation: {0}")]
Invalid(String),
}
pub struct ProjectManager {
projects: RwLock<HashMap<ProjectId, Project>>,
name_index: RwLock<HashMap<String, ProjectId>>,
db: Arc<MemoryDatabase>,
event_bus: Option<EventBus>,
}
impl ProjectManager {
pub fn new(db: Arc<MemoryDatabase>, event_bus: Option<EventBus>) -> Result<Self> {
let mut projects = HashMap::new();
let mut name_index = HashMap::new();
let rows = db.list_projects()?;
for project in rows {
name_index.insert(project.name.clone(), project.id);
projects.insert(project.id, project);
}
tracing::info!(count = projects.len(), "ProjectManager initialized");
Ok(Self {
projects: RwLock::new(projects),
name_index: RwLock::new(name_index),
db,
event_bus,
})
}
pub fn list_projects(&self) -> Vec<Project> {
self.projects.read().values().cloned().collect()
}
pub fn get_project(&self, id: ProjectId) -> Option<Project> {
self.projects.read().get(&id).cloned()
}
pub fn get_project_by_name(&self, name: &str) -> Option<Project> {
let name_index = self.name_index.read();
let id = name_index.get(name)?;
self.projects.read().get(id).cloned()
}
pub fn create_project(
&self,
name: String,
paths: Vec<PathBuf>,
tags: Vec<String>,
emoji: Option<String>,
description: Option<String>,
source: ProjectSource,
) -> Result<Project> {
{
let name_index = self.name_index.read();
if name_index.contains_key(&name) {
return Err(ProjectManagerError::DuplicateName(name).into());
}
}
let mut project = Project::new(&name, source);
project.paths = paths;
project.tags = tags;
if let Some(emoji) = emoji {
project.emoji = emoji;
}
if let Some(description) = description {
project.description = description;
}
self.db.save_project(&project)?;
{
let mut projects = self.projects.write();
let mut name_index = self.name_index.write();
name_index.insert(project.name.clone(), project.id);
projects.insert(project.id, project.clone());
}
if let Some(ref event_bus) = self.event_bus {
let _ = event_bus.publish(KernelEvent::ProjectCreated {
project_id: project.id,
name: project.name.clone(),
source: source.to_string(),
});
}
tracing::info!(name = %project.name, id = %project.id, "Project created");
Ok(project)
}
pub fn update_project(
&self,
id: ProjectId,
name: Option<String>,
paths: Option<Vec<PathBuf>>,
tags: Option<Vec<String>>,
emoji: Option<String>,
description: Option<String>,
) -> Result<Project> {
let mut projects = self.projects.write();
let mut name_index = self.name_index.write();
let project = projects
.get_mut(&id)
.ok_or(ProjectManagerError::NotFound(id))?;
if let Some(ref new_name) = name {
if *new_name != project.name {
if name_index.contains_key(new_name) {
return Err(ProjectManagerError::DuplicateName(new_name.clone()).into());
}
name_index.remove(&project.name);
name_index.insert(new_name.clone(), id);
project.name = new_name.clone();
}
}
if let Some(paths) = paths {
project.paths = paths;
}
if let Some(tags) = tags {
project.tags = tags;
}
if let Some(emoji) = emoji {
project.emoji = emoji;
}
if let Some(description) = description {
project.description = description;
}
project.updated_at = Utc::now();
let project_clone = project.clone();
drop(projects);
drop(name_index);
self.db.save_project(&project_clone)?;
tracing::info!(name = %project_clone.name, id = %id, "Project updated");
Ok(project_clone)
}
pub fn remove_project(&self, id: ProjectId) -> Result<()> {
{
let mut projects = self.projects.write();
let mut name_index = self.name_index.write();
let project = projects
.remove(&id)
.ok_or(ProjectManagerError::NotFound(id))?;
name_index.remove(&project.name);
}
self.db.delete_project(&id.to_string())?;
tracing::info!(id = %id, "Project removed");
Ok(())
}
pub fn touch(&self, id: ProjectId) {
if let Some(project) = self.projects.write().get_mut(&id) {
project.touch();
let project_clone = project.clone();
drop(self.projects.write());
let _ = self.db.save_project(&project_clone);
}
}
pub fn detect(&self, message: &str) -> DetectionResult {
let projects = self.list_projects();
detect_project(message, &projects)
}
pub fn link_memory(&self, project_id: ProjectId, memory_id: &str) -> Result<()> {
{
let projects = self.projects.read();
if !projects.contains_key(&project_id) {
return Err(ProjectManagerError::NotFound(project_id).into());
}
}
self.db
.link_project_memory(&project_id.to_string(), memory_id)?;
Ok(())
}
pub fn unlink_memory(&self, project_id: ProjectId, memory_id: &str) -> Result<()> {
self.db
.unlink_project_memory(&project_id.to_string(), memory_id)?;
Ok(())
}
pub fn get_project_memory_ids(&self, project_id: ProjectId) -> Result<Vec<String>> {
self.db.get_project_memory_ids(&project_id.to_string())
}
pub fn save_project(&self, project: &Project) -> Result<()> {
self.db.save_project(project)?;
let mut projects = self.projects.write();
let mut name_index = self.name_index.write();
name_index.insert(project.name.clone(), project.id);
projects.insert(project.id, project.clone());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_project_manager_error_display() {
let id = ProjectId::new_v4();
let err = ProjectManagerError::NotFound(id);
assert!(err.to_string().contains("Project not found"));
let err = ProjectManagerError::DuplicateName("test".to_string());
assert!(err.to_string().contains("already exists"));
}
}