package service import ( "context" "encoding/json" "github.com/google/uuid" "tanabata/backend/internal/domain" "tanabata/backend/internal/port" ) const poolObjectType = "pool" const poolObjectTypeID int16 = 4 // fourth row in 007_seed_data.sql object_types // PoolParams holds the fields for creating or patching a pool. type PoolParams struct { Name string Notes *string Metadata json.RawMessage IsPublic *bool } // PoolService handles pool CRUD and pool–file management with ACL + audit. type PoolService struct { pools port.PoolRepo acl *ACLService audit *AuditService } // NewPoolService creates a PoolService. func NewPoolService( pools port.PoolRepo, acl *ACLService, audit *AuditService, ) *PoolService { return &PoolService{pools: pools, acl: acl, audit: audit} } // --------------------------------------------------------------------------- // CRUD // --------------------------------------------------------------------------- // List returns a paginated list of pools. func (s *PoolService) List(ctx context.Context, params port.OffsetParams) (*domain.PoolOffsetPage, error) { return s.pools.List(ctx, params) } // Get returns a pool by ID. func (s *PoolService) Get(ctx context.Context, id uuid.UUID) (*domain.Pool, error) { return s.pools.GetByID(ctx, id) } // Create inserts a new pool. func (s *PoolService) Create(ctx context.Context, p PoolParams) (*domain.Pool, error) { userID, _, _ := domain.UserFromContext(ctx) pool := &domain.Pool{ Name: p.Name, Notes: p.Notes, Metadata: p.Metadata, CreatorID: userID, } if p.IsPublic != nil { pool.IsPublic = *p.IsPublic } created, err := s.pools.Create(ctx, pool) if err != nil { return nil, err } objType := poolObjectType _ = s.audit.Log(ctx, "pool_create", &objType, &created.ID, nil) return created, nil } // Update applies a partial patch to a pool. func (s *PoolService) Update(ctx context.Context, id uuid.UUID, p PoolParams) (*domain.Pool, error) { userID, isAdmin, _ := domain.UserFromContext(ctx) current, err := s.pools.GetByID(ctx, id) if err != nil { return nil, err } ok, err := s.acl.CanEdit(ctx, userID, isAdmin, current.CreatorID, poolObjectTypeID, id) if err != nil { return nil, err } if !ok { return nil, domain.ErrForbidden } patch := *current if p.Name != "" { patch.Name = p.Name } if p.Notes != nil { patch.Notes = p.Notes } if len(p.Metadata) > 0 { patch.Metadata = p.Metadata } if p.IsPublic != nil { patch.IsPublic = *p.IsPublic } updated, err := s.pools.Update(ctx, id, &patch) if err != nil { return nil, err } objType := poolObjectType _ = s.audit.Log(ctx, "pool_edit", &objType, &id, nil) return updated, nil } // Delete removes a pool by ID, enforcing edit ACL. func (s *PoolService) Delete(ctx context.Context, id uuid.UUID) error { userID, isAdmin, _ := domain.UserFromContext(ctx) pool, err := s.pools.GetByID(ctx, id) if err != nil { return err } ok, err := s.acl.CanEdit(ctx, userID, isAdmin, pool.CreatorID, poolObjectTypeID, id) if err != nil { return err } if !ok { return domain.ErrForbidden } if err := s.pools.Delete(ctx, id); err != nil { return err } objType := poolObjectType _ = s.audit.Log(ctx, "pool_delete", &objType, &id, nil) return nil } // --------------------------------------------------------------------------- // Pool–file operations // --------------------------------------------------------------------------- // ListFiles returns cursor-paginated files within a pool ordered by position. func (s *PoolService) ListFiles(ctx context.Context, poolID uuid.UUID, params port.PoolFileListParams) (*domain.PoolFilePage, error) { return s.pools.ListFiles(ctx, poolID, params) } // AddFiles adds files to a pool at the given position (nil = append). func (s *PoolService) AddFiles(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID, position *int) error { if err := s.pools.AddFiles(ctx, poolID, fileIDs, position); err != nil { return err } objType := poolObjectType _ = s.audit.Log(ctx, "file_pool_add", &objType, &poolID, map[string]any{"count": len(fileIDs)}) return nil } // RemoveFiles removes files from a pool. func (s *PoolService) RemoveFiles(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID) error { if err := s.pools.RemoveFiles(ctx, poolID, fileIDs); err != nil { return err } objType := poolObjectType _ = s.audit.Log(ctx, "file_pool_remove", &objType, &poolID, map[string]any{"count": len(fileIDs)}) return nil } // Reorder sets the full ordered sequence of file IDs within a pool. func (s *PoolService) Reorder(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID) error { return s.pools.Reorder(ctx, poolID, fileIDs) }