package repository import ( "context" "strings" "time" "github.com/gochat/gochat/internal/model" "gorm.io/gorm" ) // CaptainDocumentRepo provides data access for CaptainDocument. type CaptainDocumentRepo struct { db *gorm.DB } func NewCaptainDocumentRepo(db *gorm.DB) *CaptainDocumentRepo { return &CaptainDocumentRepo{db: db} } func (r *CaptainDocumentRepo) DB() *gorm.DB { return r.db } func (r *CaptainDocumentRepo) Create(ctx context.Context, doc *model.CaptainDocument) error { return r.db.WithContext(ctx).Create(doc).Error } func (r *CaptainDocumentRepo) GetByID(ctx context.Context, id uint) (*model.CaptainDocument, error) { var doc model.CaptainDocument if err := r.db.WithContext(ctx).Preload("Assistant").First(&doc, id).Error; err != nil { return nil, err } return &doc, nil } func (r *CaptainDocumentRepo) GetByAccountAndID(ctx context.Context, accountID, id uint) (*model.CaptainDocument, error) { var doc model.CaptainDocument if err := r.db.WithContext(ctx).Preload("Assistant").Where("account_id = ? AND id = ?", accountID, id).First(&doc).Error; err != nil { return nil, err } return &doc, nil } func (r *CaptainDocumentRepo) Update(ctx context.Context, doc *model.CaptainDocument) error { return r.db.WithContext(ctx).Save(doc).Error } func (r *CaptainDocumentRepo) Delete(ctx context.Context, id uint) error { return r.db.WithContext(ctx).Delete(&model.CaptainDocument{}, id).Error } func (r *CaptainDocumentRepo) DeleteByAccount(ctx context.Context, accountID, id uint) error { return r.db.WithContext(ctx).Where("account_id = ? AND id = ?", accountID, id).Delete(&model.CaptainDocument{}).Error } func (r *CaptainDocumentRepo) ListByAssistant(ctx context.Context, assistantID uint, offset, limit int) ([]model.CaptainDocument, int64, error) { var docs []model.CaptainDocument var count int64 db := r.db.WithContext(ctx).Model(&model.CaptainDocument{}).Where("assistant_id = ?", assistantID) db.Count(&count) if err := db.Preload("Assistant").Offset(offset).Limit(limit).Find(&docs).Error; err != nil { return nil, 0, err } return docs, count, nil } type CaptainDocumentListFilters struct { AssistantID uint Source string Filter string SearchKey string Sort string Offset int Limit int } func (r *CaptainDocumentRepo) ListByAccount(ctx context.Context, accountID uint, filters CaptainDocumentListFilters) ([]model.CaptainDocument, int64, error) { var docs []model.CaptainDocument var count int64 db := r.db.WithContext(ctx).Model(&model.CaptainDocument{}).Where("account_id = ?", accountID) if filters.AssistantID > 0 { db = db.Where("assistant_id = ?", filters.AssistantID) } switch filters.Source { case "web": db = db.Where("external_link <> ''") case "pdf": db = db.Where("external_link = ''") } switch filters.Filter { case "syncing": db = db.Where("sync_status = ?", model.DocumentSyncStatusPending) case "failed": db = db.Where("sync_status = ?", model.DocumentSyncStatusFailed) case "synced": db = db.Where("sync_status = ?", model.DocumentSyncStatusSynced) } if filters.SearchKey != "" { query := "%" + strings.ToLower(filters.SearchKey) + "%" db = db.Where("LOWER(name) LIKE ? OR LOWER(external_link) LIKE ?", query, query) } if err := db.Count(&count).Error; err != nil { return nil, 0, err } order := "updated_at DESC" if filters.Sort == "recently_created" { order = "created_at DESC" } if filters.Limit <= 0 { filters.Limit = 25 } if err := db.Preload("Assistant").Order(order).Offset(filters.Offset).Limit(filters.Limit).Find(&docs).Error; err != nil { return nil, 0, err } return docs, count, nil } func (r *CaptainDocumentRepo) FindByExternalLink(ctx context.Context, assistantID uint, externalLink string) (*model.CaptainDocument, error) { var doc model.CaptainDocument if err := r.db.WithContext(ctx).Where("assistant_id = ? AND external_link = ?", assistantID, externalLink).First(&doc).Error; err != nil { return nil, err } return &doc, nil } func (r *CaptainDocumentRepo) ListDueForAutoSync(ctx context.Context, now time.Time, interval, staleTimeout time.Duration, limit int) ([]model.CaptainDocument, error) { if interval <= 0 { interval = 24 * time.Hour } if staleTimeout <= 0 { staleTimeout = 10 * time.Minute } if limit <= 0 { limit = 1000 } syncDueBefore := now.Add(-interval / 2).Unix() staleCutoff := now.Add(-staleTimeout).Unix() var docs []model.CaptainDocument err := r.db.WithContext(ctx). Model(&model.CaptainDocument{}). Where("status = ?", model.DocumentStatusCompleted). Where( "(sync_status = ? AND (last_synced_at IS NULL OR last_synced_at < ?)) OR "+ "(sync_status = ? AND (last_sync_attempted_at IS NULL OR last_sync_attempted_at < ?)) OR "+ "(sync_status = ? AND last_sync_attempted_at < ?)", model.DocumentSyncStatusSynced, syncDueBefore, model.DocumentSyncStatusFailed, syncDueBefore, model.DocumentSyncStatusPending, staleCutoff, ). Order("last_sync_attempted_at ASC, id ASC"). Limit(limit). Find(&docs).Error return docs, err } // FindByStatus retrieves documents matching a specific status. func (r *CaptainDocumentRepo) FindByStatus(ctx context.Context, assistantID uint, status model.DocumentStatus, offset, limit int) ([]model.CaptainDocument, int64, error) { var docs []model.CaptainDocument var count int64 db := r.db.WithContext(ctx).Model(&model.CaptainDocument{}).Where("assistant_id = ? AND status = ?", assistantID, status) db.Count(&count) if err := db.Offset(offset).Limit(limit).Find(&docs).Error; err != nil { return nil, 0, err } return docs, count, nil }