package memory import ( "context" "encoding/json" "errors" "sync" "time" "qctextbuilder/internal/domain" ) var ErrNotFound = errors.New("not found") type Store struct { mu sync.RWMutex templates map[int64]domain.Template manifests map[int64]domain.TemplateManifest manifestField map[string][]domain.TemplateField builds map[string]domain.SiteBuild } func New() *Store { return &Store{ templates: make(map[int64]domain.Template), manifests: make(map[int64]domain.TemplateManifest), manifestField: make(map[string][]domain.TemplateField), builds: make(map[string]domain.SiteBuild), } } func (s *Store) UpsertTemplates(_ context.Context, templates []domain.Template) error { s.mu.Lock() defer s.mu.Unlock() for _, t := range templates { existing, ok := s.templates[t.ID] if ok { t.IsOnboarded = existing.IsOnboarded t.ManifestStatus = existing.ManifestStatus t.LastDiscoveredAt = existing.LastDiscoveredAt } s.templates[t.ID] = t } return nil } func (s *Store) GetTemplateByID(_ context.Context, id int64) (*domain.Template, error) { s.mu.RLock() defer s.mu.RUnlock() t, ok := s.templates[id] if !ok { return nil, ErrNotFound } copy := t return ©, nil } func (s *Store) ListTemplates(_ context.Context) ([]domain.Template, error) { s.mu.RLock() defer s.mu.RUnlock() out := make([]domain.Template, 0, len(s.templates)) for _, t := range s.templates { out = append(out, t) } return out, nil } func (s *Store) SetTemplateManifestStatus(_ context.Context, templateID int64, status string, onboarded bool) error { s.mu.Lock() defer s.mu.Unlock() t, ok := s.templates[templateID] if !ok { return ErrNotFound } t.ManifestStatus = status t.IsOnboarded = onboarded s.templates[templateID] = t return nil } func (s *Store) CreateManifest(_ context.Context, manifest domain.TemplateManifest, fields []domain.TemplateField) error { s.mu.Lock() defer s.mu.Unlock() s.manifests[manifest.TemplateID] = manifest s.manifestField[manifest.ID] = fields return nil } func (s *Store) GetActiveManifestByTemplateID(_ context.Context, templateID int64) (*domain.TemplateManifest, error) { s.mu.RLock() defer s.mu.RUnlock() m, ok := s.manifests[templateID] if !ok { return nil, ErrNotFound } copy := m return ©, nil } func (s *Store) ListFieldsByManifestID(_ context.Context, manifestID string) ([]domain.TemplateField, error) { s.mu.RLock() defer s.mu.RUnlock() fields, ok := s.manifestField[manifestID] if !ok { return nil, ErrNotFound } out := make([]domain.TemplateField, 0, len(fields)) out = append(out, fields...) return out, nil } func (s *Store) UpdateFields(_ context.Context, manifestID string, fields []domain.TemplateField) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.manifestField[manifestID]; !ok { return ErrNotFound } next := make([]domain.TemplateField, len(fields)) copy(next, fields) s.manifestField[manifestID] = next return nil } func (s *Store) CreateBuild(_ context.Context, build domain.SiteBuild) error { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.builds[build.ID]; exists { return errors.New("build already exists") } s.builds[build.ID] = build return nil } func (s *Store) GetBuildByID(_ context.Context, id string) (*domain.SiteBuild, error) { s.mu.RLock() defer s.mu.RUnlock() build, ok := s.builds[id] if !ok { return nil, ErrNotFound } copy := build return ©, nil } func (s *Store) ListBuildsByStatuses(_ context.Context, statuses []string, limit int) ([]domain.SiteBuild, error) { s.mu.RLock() defer s.mu.RUnlock() allowed := make(map[string]struct{}, len(statuses)) for _, status := range statuses { allowed[status] = struct{}{} } out := make([]domain.SiteBuild, 0) for _, build := range s.builds { if len(allowed) > 0 { if _, ok := allowed[build.QCStatus]; !ok { continue } } out = append(out, build) if limit > 0 && len(out) >= limit { break } } return out, nil } func (s *Store) MarkBuildSubmitted(_ context.Context, buildID string, jobID int64, status string, qcResult json.RawMessage, startedAt time.Time) error { s.mu.Lock() defer s.mu.Unlock() build, ok := s.builds[buildID] if !ok { return ErrNotFound } build.QCJobID = &jobID build.QCStatus = status build.QCResultJSON = cloneRaw(qcResult) build.StartedAt = &startedAt s.builds[buildID] = build return nil } func (s *Store) UpdateBuildFromJob(_ context.Context, buildID string, status string, siteID *int64, previewURL string, qcResult json.RawMessage, qcError json.RawMessage, finishedAt *time.Time) error { s.mu.Lock() defer s.mu.Unlock() build, ok := s.builds[buildID] if !ok { return ErrNotFound } build.QCStatus = status build.QCResultJSON = cloneRaw(qcResult) build.QCErrorJSON = cloneRaw(qcError) build.QCPreviewURL = previewURL if siteID != nil { id := *siteID build.QCSiteID = &id } build.FinishedAt = finishedAt s.builds[buildID] = build return nil } func (s *Store) UpdateBuildEditorURL(_ context.Context, buildID string, editorURL string, qcResult json.RawMessage) error { s.mu.Lock() defer s.mu.Unlock() build, ok := s.builds[buildID] if !ok { return ErrNotFound } build.QCEditorURL = editorURL build.QCResultJSON = cloneRaw(qcResult) s.builds[buildID] = build return nil } func cloneRaw(raw json.RawMessage) json.RawMessage { if raw == nil { return nil } out := make([]byte, len(raw)) copy(out, raw) return json.RawMessage(out) }