package memory import ( "context" "encoding/json" "errors" "sort" "sync" "time" "qctextbuilder/internal/domain" "qctextbuilder/internal/store" ) 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 drafts map[string]domain.BuildDraft settings domain.AppSettings hasSettings bool } 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), drafts: make(map[string]domain.BuildDraft), } } 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, store.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 store.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, store.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, store.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 store.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, store.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 store.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 store.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 store.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) } func (s *Store) UpsertSettings(_ context.Context, settings domain.AppSettings) error { s.mu.Lock() defer s.mu.Unlock() s.settings = settings s.hasSettings = true return nil } func (s *Store) GetSettings(_ context.Context) (*domain.AppSettings, error) { s.mu.RLock() defer s.mu.RUnlock() if !s.hasSettings { return nil, store.ErrNotFound } value := s.settings return &value, nil } func (s *Store) CreateDraft(_ context.Context, draft domain.BuildDraft) error { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.drafts[draft.ID]; exists { return errors.New("draft already exists") } s.drafts[draft.ID] = draft return nil } func (s *Store) UpdateDraft(_ context.Context, draft domain.BuildDraft) error { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.drafts[draft.ID]; !exists { return store.ErrNotFound } s.drafts[draft.ID] = draft return nil } func (s *Store) GetDraftByID(_ context.Context, id string) (*domain.BuildDraft, error) { s.mu.RLock() defer s.mu.RUnlock() draft, ok := s.drafts[id] if !ok { return nil, store.ErrNotFound } copy := draft copy.GlobalDataJSON = cloneRaw(draft.GlobalDataJSON) copy.FieldValuesJSON = cloneRaw(draft.FieldValuesJSON) copy.DraftContextJSON = cloneRaw(draft.DraftContextJSON) return ©, nil } func (s *Store) ListDrafts(_ context.Context, limit int) ([]domain.BuildDraft, error) { s.mu.RLock() defer s.mu.RUnlock() out := make([]domain.BuildDraft, 0, len(s.drafts)) for _, draft := range s.drafts { copy := draft copy.GlobalDataJSON = cloneRaw(draft.GlobalDataJSON) copy.FieldValuesJSON = cloneRaw(draft.FieldValuesJSON) copy.DraftContextJSON = cloneRaw(draft.DraftContextJSON) out = append(out, copy) } sort.Slice(out, func(i, j int) bool { return out[i].UpdatedAt.After(out[j].UpdatedAt) }) if limit > 0 && len(out) > limit { out = out[:limit] } return out, nil }