package memory import ( "context" "fmt" "sync" "time" "git.threesix.ai/jordan/persona-community-3/pkg/album" ) // AlbumRepository is an in-memory implementation of port.AlbumRepository. // Used in standalone dev mode (no DATABASE_URL). Not safe for persistence across restarts. type AlbumRepository struct { mu sync.RWMutex albums map[album.AlbumID]*album.Album } // NewAlbumRepository creates an in-memory album repository. func NewAlbumRepository() *AlbumRepository { return &AlbumRepository{ albums: make(map[album.AlbumID]*album.Album), } } // Create persists a new album. The caller must set ID, Name, SubjectDesc, Shots before calling. func (r *AlbumRepository) Create(ctx context.Context, a *album.Album) error { r.mu.Lock() defer r.mu.Unlock() now := time.Now().UTC() a.CreatedAt = now a.UpdatedAt = now copy := *a r.albums[a.ID] = © return nil } // Get returns an album by ID and userID. Returns error if not found or wrong user. func (r *AlbumRepository) Get(ctx context.Context, id album.AlbumID, userID string) (*album.Album, error) { r.mu.RLock() defer r.mu.RUnlock() a, ok := r.albums[id] if !ok || a.UserID != userID { return nil, fmt.Errorf("album not found: %s", id) } copy := *a shots := make([]album.Shot, len(a.Shots)) copy.Shots = shots for i, s := range a.Shots { shots[i] = s } return ©, nil } // List returns all albums for a user, ordered by CreatedAt DESC. func (r *AlbumRepository) List(ctx context.Context, userID string) ([]album.Album, error) { r.mu.RLock() defer r.mu.RUnlock() var result []album.Album for _, a := range r.albums { if a.UserID != userID { continue } copy := *a shots := make([]album.Shot, len(a.Shots)) for i, s := range a.Shots { shots[i] = s } copy.Shots = shots result = append(result, copy) } // Sort by CreatedAt DESC (simple insertion sort — in-memory is small). for i := 1; i < len(result); i++ { for j := i; j > 0 && result[j].CreatedAt.After(result[j-1].CreatedAt); j-- { result[j], result[j-1] = result[j-1], result[j] } } return result, nil } // Delete removes an album by ID and userID. func (r *AlbumRepository) Delete(ctx context.Context, id album.AlbumID, userID string) error { r.mu.Lock() defer r.mu.Unlock() a, ok := r.albums[id] if !ok || a.UserID != userID { return fmt.Errorf("album not found: %s", id) } delete(r.albums, id) return nil } // UpdateAnchor stores the generated anchor URL. func (r *AlbumRepository) UpdateAnchor(ctx context.Context, id album.AlbumID, userID, anchorURL, anchorJobID string) error { r.mu.Lock() defer r.mu.Unlock() a, ok := r.albums[id] if !ok || a.UserID != userID { return fmt.Errorf("album not found: %s", id) } a.AnchorURL = anchorURL a.AnchorJobID = anchorJobID a.UpdatedAt = time.Now().UTC() return nil } // UpdateShot stores the generated image URL and status for a specific shot. func (r *AlbumRepository) UpdateShot(ctx context.Context, id album.AlbumID, userID string, shotIndex int, imageURL string, status album.ShotStatus, shotError string) error { r.mu.Lock() defer r.mu.Unlock() a, ok := r.albums[id] if !ok || a.UserID != userID { return fmt.Errorf("album not found: %s", id) } if shotIndex < 0 || shotIndex >= len(a.Shots) { return fmt.Errorf("shot index out of range: %d", shotIndex) } now := time.Now().UTC() a.Shots[shotIndex].ImageURL = imageURL a.Shots[shotIndex].Status = status a.Shots[shotIndex].Error = shotError if status == album.ShotComplete { a.Shots[shotIndex].GeneratedAt = &now } a.UpdatedAt = now return nil } // ResetShot clears a shot back to pending. func (r *AlbumRepository) ResetShot(ctx context.Context, id album.AlbumID, userID string, shotIndex int) error { r.mu.Lock() defer r.mu.Unlock() a, ok := r.albums[id] if !ok || a.UserID != userID { return fmt.Errorf("album not found: %s", id) } if shotIndex < 0 || shotIndex >= len(a.Shots) { return fmt.Errorf("shot index out of range: %d", shotIndex) } a.Shots[shotIndex].ImageURL = "" a.Shots[shotIndex].JobID = "" a.Shots[shotIndex].Status = album.ShotPending a.Shots[shotIndex].Error = "" a.Shots[shotIndex].GeneratedAt = nil a.UpdatedAt = time.Now().UTC() return nil } // UpdateAnchorJobID stores the anchor job ID when the anchor generation is enqueued. func (r *AlbumRepository) UpdateAnchorJobID(ctx context.Context, id album.AlbumID, userID, jobID string) error { r.mu.Lock() defer r.mu.Unlock() a, ok := r.albums[id] if !ok || a.UserID != userID { return fmt.Errorf("album not found: %s", id) } a.AnchorJobID = jobID a.UpdatedAt = time.Now().UTC() return nil } // UpdateShotJobID stores the job ID for a shot when its generation is enqueued. func (r *AlbumRepository) UpdateShotJobID(ctx context.Context, id album.AlbumID, userID string, shotIndex int, jobID string) error { r.mu.Lock() defer r.mu.Unlock() a, ok := r.albums[id] if !ok || a.UserID != userID { return fmt.Errorf("album not found: %s", id) } if shotIndex < 0 || shotIndex >= len(a.Shots) { return fmt.Errorf("shot index out of range: %d", shotIndex) } a.Shots[shotIndex].JobID = jobID a.Shots[shotIndex].Status = album.ShotGenerating a.UpdatedAt = time.Now().UTC() return nil }