package services import ( "encoding/json" "errors" "time" "github.com/google/uuid" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/services/sync" ) // LocalConfigurationService handles configurations in local-first mode // All operations go through SQLite, MariaDB is used only for sync type LocalConfigurationService struct { localDB *localdb.LocalDB syncService *sync.Service quoteService *QuoteService isOnline func() bool // Function to check if we're online } // NewLocalConfigurationService creates a new local-first configuration service func NewLocalConfigurationService( localDB *localdb.LocalDB, syncService *sync.Service, quoteService *QuoteService, isOnline func() bool, ) *LocalConfigurationService { return &LocalConfigurationService{ localDB: localDB, syncService: syncService, quoteService: quoteService, isOnline: isOnline, } } // Create creates a new configuration in local SQLite and queues it for sync func (s *LocalConfigurationService) Create(userID uint, req *CreateConfigRequest) (*models.Configuration, error) { // If online, check for new pricelists first if s.isOnline() { if err := s.syncService.SyncPricelistsIfNeeded(); err != nil { // Log but don't fail - we can still use local pricelists } } total := req.Items.Total() if req.ServerCount > 1 { total *= float64(req.ServerCount) } cfg := &models.Configuration{ UUID: uuid.New().String(), UserID: userID, Name: req.Name, Items: req.Items, TotalPrice: &total, CustomPrice: req.CustomPrice, Notes: req.Notes, IsTemplate: req.IsTemplate, ServerCount: req.ServerCount, CreatedAt: time.Now(), } // Convert to local model localCfg := localdb.ConfigurationToLocal(cfg) // Save to local SQLite if err := s.localDB.SaveConfiguration(localCfg); err != nil { return nil, err } // Add to pending sync queue payload, err := json.Marshal(cfg) if err != nil { return nil, err } if err := s.localDB.AddPendingChange("configuration", cfg.UUID, "create", string(payload)); err != nil { return nil, err } // Record usage stats _ = s.quoteService.RecordUsage(req.Items) return cfg, nil } // GetByUUID returns a configuration from local SQLite func (s *LocalConfigurationService) GetByUUID(uuid string, userID uint) (*models.Configuration, error) { localCfg, err := s.localDB.GetConfigurationByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } // Convert to models.Configuration cfg := localdb.LocalToConfiguration(localCfg) // Allow access if user owns config or it's a template if cfg.UserID != userID && !cfg.IsTemplate { return nil, ErrConfigForbidden } return cfg, nil } // Update updates a configuration in local SQLite and queues it for sync func (s *LocalConfigurationService) Update(uuid string, userID uint, req *CreateConfigRequest) (*models.Configuration, error) { localCfg, err := s.localDB.GetConfigurationByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } if localCfg.OriginalUserID != userID { return nil, ErrConfigForbidden } total := req.Items.Total() if req.ServerCount > 1 { total *= float64(req.ServerCount) } // Update fields localCfg.Name = req.Name localCfg.Items = localdb.LocalConfigItems{} for _, item := range req.Items { localCfg.Items = append(localCfg.Items, localdb.LocalConfigItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: item.UnitPrice, }) } localCfg.TotalPrice = &total localCfg.CustomPrice = req.CustomPrice localCfg.Notes = req.Notes localCfg.IsTemplate = req.IsTemplate localCfg.ServerCount = req.ServerCount localCfg.UpdatedAt = time.Now() localCfg.SyncStatus = "pending" // Save to local SQLite if err := s.localDB.SaveConfiguration(localCfg); err != nil { return nil, err } // Add to pending sync queue cfg := localdb.LocalToConfiguration(localCfg) payload, err := json.Marshal(cfg) if err != nil { return nil, err } if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil { return nil, err } return cfg, nil } // Delete deletes a configuration from local SQLite and queues it for sync func (s *LocalConfigurationService) Delete(uuid string, userID uint) error { localCfg, err := s.localDB.GetConfigurationByUUID(uuid) if err != nil { return ErrConfigNotFound } if localCfg.OriginalUserID != userID { return ErrConfigForbidden } // Delete from local SQLite if err := s.localDB.DeleteConfiguration(uuid); err != nil { return err } // Add to pending sync queue if err := s.localDB.AddPendingChange("configuration", uuid, "delete", ""); err != nil { return err } return nil } // Rename renames a configuration func (s *LocalConfigurationService) Rename(uuid string, userID uint, newName string) (*models.Configuration, error) { localCfg, err := s.localDB.GetConfigurationByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } if localCfg.OriginalUserID != userID { return nil, ErrConfigForbidden } localCfg.Name = newName localCfg.UpdatedAt = time.Now() localCfg.SyncStatus = "pending" if err := s.localDB.SaveConfiguration(localCfg); err != nil { return nil, err } // Add to pending sync queue cfg := localdb.LocalToConfiguration(localCfg) payload, err := json.Marshal(cfg) if err != nil { return nil, err } if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil { return nil, err } return cfg, nil } // Clone clones a configuration func (s *LocalConfigurationService) Clone(configUUID string, userID uint, newName string) (*models.Configuration, error) { original, err := s.GetByUUID(configUUID, userID) if err != nil { return nil, err } total := original.Items.Total() if original.ServerCount > 1 { total *= float64(original.ServerCount) } clone := &models.Configuration{ UUID: uuid.New().String(), UserID: userID, Name: newName, Items: original.Items, TotalPrice: &total, CustomPrice: original.CustomPrice, Notes: original.Notes, IsTemplate: false, ServerCount: original.ServerCount, CreatedAt: time.Now(), } localCfg := localdb.ConfigurationToLocal(clone) if err := s.localDB.SaveConfiguration(localCfg); err != nil { return nil, err } // Add to pending sync queue payload, err := json.Marshal(clone) if err != nil { return nil, err } if err := s.localDB.AddPendingChange("configuration", clone.UUID, "create", string(payload)); err != nil { return nil, err } return clone, nil } // ListByUser returns all configurations for a user from local SQLite func (s *LocalConfigurationService) ListByUser(userID uint, page, perPage int) ([]models.Configuration, int64, error) { // Get all local configurations localConfigs, err := s.localDB.GetConfigurations() if err != nil { return nil, 0, err } // Filter by user var userConfigs []models.Configuration for _, lc := range localConfigs { if lc.OriginalUserID == userID || lc.IsTemplate { userConfigs = append(userConfigs, *localdb.LocalToConfiguration(&lc)) } } total := int64(len(userConfigs)) // Apply pagination if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 20 } offset := (page - 1) * perPage start := offset if start > len(userConfigs) { start = len(userConfigs) } end := start + perPage if end > len(userConfigs) { end = len(userConfigs) } return userConfigs[start:end], total, nil } // RefreshPrices updates all component prices in the configuration func (s *LocalConfigurationService) RefreshPrices(uuid string, userID uint) (*models.Configuration, error) { // This requires access to component prices from local cache // For now, return error as we need to implement component price lookup from local cache return nil, errors.New("refresh prices not yet implemented for local-first mode") } // GetByUUIDNoAuth returns configuration without ownership check func (s *LocalConfigurationService) GetByUUIDNoAuth(uuid string) (*models.Configuration, error) { localCfg, err := s.localDB.GetConfigurationByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } return localdb.LocalToConfiguration(localCfg), nil } // UpdateNoAuth updates configuration without ownership check func (s *LocalConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigRequest) (*models.Configuration, error) { localCfg, err := s.localDB.GetConfigurationByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } total := req.Items.Total() if req.ServerCount > 1 { total *= float64(req.ServerCount) } localCfg.Name = req.Name localCfg.Items = localdb.LocalConfigItems{} for _, item := range req.Items { localCfg.Items = append(localCfg.Items, localdb.LocalConfigItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: item.UnitPrice, }) } localCfg.TotalPrice = &total localCfg.CustomPrice = req.CustomPrice localCfg.Notes = req.Notes localCfg.IsTemplate = req.IsTemplate localCfg.ServerCount = req.ServerCount localCfg.UpdatedAt = time.Now() localCfg.SyncStatus = "pending" if err := s.localDB.SaveConfiguration(localCfg); err != nil { return nil, err } cfg := localdb.LocalToConfiguration(localCfg) payload, err := json.Marshal(cfg) if err != nil { return nil, err } if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil { return nil, err } return cfg, nil } // DeleteNoAuth deletes configuration without ownership check func (s *LocalConfigurationService) DeleteNoAuth(uuid string) error { if err := s.localDB.DeleteConfiguration(uuid); err != nil { return err } return s.localDB.AddPendingChange("configuration", uuid, "delete", "") } // RenameNoAuth renames configuration without ownership check func (s *LocalConfigurationService) RenameNoAuth(uuid string, newName string) (*models.Configuration, error) { localCfg, err := s.localDB.GetConfigurationByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } localCfg.Name = newName localCfg.UpdatedAt = time.Now() localCfg.SyncStatus = "pending" if err := s.localDB.SaveConfiguration(localCfg); err != nil { return nil, err } cfg := localdb.LocalToConfiguration(localCfg) payload, err := json.Marshal(cfg) if err != nil { return nil, err } if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil { return nil, err } return cfg, nil } // CloneNoAuth clones configuration without ownership check func (s *LocalConfigurationService) CloneNoAuth(configUUID string, newName string, userID uint) (*models.Configuration, error) { original, err := s.GetByUUIDNoAuth(configUUID) if err != nil { return nil, err } total := original.Items.Total() if original.ServerCount > 1 { total *= float64(original.ServerCount) } clone := &models.Configuration{ UUID: uuid.New().String(), UserID: userID, Name: newName, Items: original.Items, TotalPrice: &total, CustomPrice: original.CustomPrice, Notes: original.Notes, IsTemplate: false, ServerCount: original.ServerCount, CreatedAt: time.Now(), } localCfg := localdb.ConfigurationToLocal(clone) if err := s.localDB.SaveConfiguration(localCfg); err != nil { return nil, err } payload, err := json.Marshal(clone) if err != nil { return nil, err } if err := s.localDB.AddPendingChange("configuration", clone.UUID, "create", string(payload)); err != nil { return nil, err } return clone, nil } // ListAll returns all configurations without user filter func (s *LocalConfigurationService) ListAll(page, perPage int) ([]models.Configuration, int64, error) { localConfigs, err := s.localDB.GetConfigurations() if err != nil { return nil, 0, err } configs := make([]models.Configuration, len(localConfigs)) for i, lc := range localConfigs { configs[i] = *localdb.LocalToConfiguration(&lc) } total := int64(len(configs)) // Apply pagination if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 20 } offset := (page - 1) * perPage start := offset if start > len(configs) { start = len(configs) } end := start + perPage if end > len(configs) { end = len(configs) } return configs[start:end], total, nil } // ListTemplates returns all template configurations func (s *LocalConfigurationService) ListTemplates(page, perPage int) ([]models.Configuration, int64, error) { localConfigs, err := s.localDB.GetConfigurations() if err != nil { return nil, 0, err } var templates []models.Configuration for _, lc := range localConfigs { if lc.IsTemplate { templates = append(templates, *localdb.LocalToConfiguration(&lc)) } } total := int64(len(templates)) // Apply pagination if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 20 } offset := (page - 1) * perPage start := offset if start > len(templates) { start = len(templates) } end := start + perPage if end > len(templates) { end = len(templates) } return templates[start:end], total, nil } // RefreshPricesNoAuth updates all component prices in the configuration without ownership check func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configuration, error) { // This requires access to component prices from local cache // For now, return error as we need to implement component price lookup from local cache return nil, errors.New("refresh prices not yet implemented for local-first mode") }