package project import ( "fmt" "os" "path/filepath" "sync" "time" "engimind/internal/models" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // ProjectService manages multi-project lifecycle with isolated SQLite databases. type ProjectService struct { globalDB *gorm.DB projectDB *gorm.DB currentID string mu sync.Mutex baseDir string } // NewProjectService creates a project service. Call SetGlobalDB before use. func NewProjectService() *ProjectService { homeDir, _ := os.UserHomeDir() return &ProjectService{ baseDir: filepath.Join(homeDir, ".engimind", "projects"), } } // SetGlobalDB injects the global database reference. func (s *ProjectService) SetGlobalDB(db *gorm.DB) { s.globalDB = db } // ListProjects returns all registered projects. func (s *ProjectService) ListProjects() ([]models.Project, error) { var projects []models.Project err := s.globalDB.Order("created_at DESC").Find(&projects).Error return projects, err } // CreateProject creates a new project with its own SQLite database. func (s *ProjectService) CreateProject(name string) (*models.Project, error) { id := fmt.Sprintf("p-%d", time.Now().UnixMilli()) projDir := filepath.Join(s.baseDir, id) if err := os.MkdirAll(projDir, 0755); err != nil { return nil, fmt.Errorf("create project dir: %w", err) } dbPath := filepath.Join(projDir, "project.db") proj := models.Project{ ID: id, Name: name, Path: dbPath, } if err := s.globalDB.Create(&proj).Error; err != nil { return nil, err } // Initialize project DB if err := s.openProjectDB(dbPath); err != nil { return nil, err } s.currentID = id return &proj, nil } // SwitchProject closes current project DB and opens a new one. func (s *ProjectService) SwitchProject(id string) error { s.mu.Lock() defer s.mu.Unlock() if s.currentID == id && s.projectDB != nil { return nil } // Close current s.closeCurrentDB() // Find project var proj models.Project if err := s.globalDB.First(&proj, "id = ?", id).Error; err != nil { return fmt.Errorf("project not found: %w", err) } if err := s.openProjectDB(proj.Path); err != nil { return err } s.currentID = id return nil } // DeleteProject removes the project and its database files. func (s *ProjectService) DeleteProject(id string) error { s.mu.Lock() defer s.mu.Unlock() if s.currentID == id { s.closeCurrentDB() } var proj models.Project if err := s.globalDB.First(&proj, "id = ?", id).Error; err != nil { return err } // Remove DB files projDir := filepath.Dir(proj.Path) os.RemoveAll(projDir) return s.globalDB.Delete(&proj).Error } // GetCurrentProjectID returns the active project ID. func (s *ProjectService) GetCurrentProjectID() string { return s.currentID } // GetProjectDB returns the current project's database. func (s *ProjectService) GetProjectDB() *gorm.DB { return s.projectDB } func (s *ProjectService) openProjectDB(dbPath string) error { db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return fmt.Errorf("open project db: %w", err) } if err := db.AutoMigrate( &models.SourceFile{}, &models.ChatMessage{}, &models.TemplateChapter{}, &models.TextChunk{}, ); err != nil { return fmt.Errorf("auto migrate project db: %w", err) } s.projectDB = db return nil } func (s *ProjectService) closeCurrentDB() { if s.projectDB != nil { sqlDB, err := s.projectDB.DB() if err == nil { sqlDB.Close() } s.projectDB = nil s.currentID = "" } }