package services

import (
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"git.mchus.pro/mchus/quoteforge/internal/appmeta"
	"git.mchus.pro/mchus/quoteforge/internal/article"
	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/models"
	"git.mchus.pro/mchus/quoteforge/internal/services/sync"
	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

var (
	ErrConfigVersionNotFound = errors.New("configuration version not found")
	ErrInvalidVersionNumber  = errors.New("invalid version number")
	ErrVersionConflict       = errors.New("configuration version conflict")
)

// 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(ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) {
	// If online, trigger pricelist sync in the background — do not block config creation
	if s.isOnline() {
		go func() {
			if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
				// Log but don't fail - we can still use local pricelists
			}
		}()
	}

	projectUUID, err := s.resolveProjectUUID(ownerUsername, req.ProjectUUID)
	if err != nil {
		return nil, err
	}
	pricelistID, err := s.resolvePricelistID(req.PricelistID)
	if err != nil {
		return nil, err
	}

	if strings.TrimSpace(req.ServerModel) != "" {
		articleResult, articleErr := article.Build(s.localDB, req.Items, article.BuildOptions{
			ServerModel:     req.ServerModel,
			SupportCode:     req.SupportCode,
			ServerPricelist: pricelistID,
		})
		if articleErr != nil {
			return nil, articleErr
		}
		req.Article = articleResult.Article
	}

	total := req.Items.Total()
	if req.ServerCount > 1 {
		total *= float64(req.ServerCount)
	}

	cfg := &models.Configuration{
		UUID:                  uuid.New().String(),
		OwnerUsername:         ownerUsername,
		ProjectUUID:           projectUUID,
		Name:                  req.Name,
		Items:                 req.Items,
		TotalPrice:            &total,
		CustomPrice:           req.CustomPrice,
		Notes:                 req.Notes,
		IsTemplate:            req.IsTemplate,
		ServerCount:           req.ServerCount,
		ServerModel:           req.ServerModel,
		SupportCode:           req.SupportCode,
		Article:               req.Article,
		PricelistID:           pricelistID,
		WarehousePricelistID:  req.WarehousePricelistID,
		CompetitorPricelistID: req.CompetitorPricelistID,
		ConfigType:            req.ConfigType,
		DisablePriceRefresh:   req.DisablePriceRefresh,
		OnlyInStock:           req.OnlyInStock,
		CreatedAt:             time.Now(),
	}
	if cfg.ConfigType == "" {
		cfg.ConfigType = "server"
	}

	// Convert to local model
	localCfg := localdb.ConfigurationToLocal(cfg)

	if err := s.createWithVersion(localCfg, ownerUsername); err != nil {
		return nil, fmt.Errorf("create configuration with version: %w", err)
	}
	cfg.Line = localCfg.Line

	return cfg, nil
}

// GetByUUID returns a configuration from local SQLite
func (s *LocalConfigurationService) GetByUUID(uuid string, ownerUsername string) (*models.Configuration, error) {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}
	if !localCfg.IsActive {
		return nil, ErrConfigNotFound
	}

	// Convert to models.Configuration
	cfg := localdb.LocalToConfiguration(localCfg)

	// Allow access if user owns config or it's a template
	if !s.isOwner(localCfg, ownerUsername) && !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, ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}

	if !s.isOwner(localCfg, ownerUsername) {
		return nil, ErrConfigForbidden
	}

	projectUUID := localCfg.ProjectUUID
	if req.ProjectUUID != nil {
		projectUUID, err = s.resolveProjectUUID(ownerUsername, req.ProjectUUID)
		if err != nil {
			return nil, err
		}
	}
	pricelistID, err := s.resolvePricelistID(req.PricelistID)
	if err != nil {
		return nil, err
	}

	if strings.TrimSpace(req.ServerModel) != "" {
		articleResult, articleErr := article.Build(s.localDB, req.Items, article.BuildOptions{
			ServerModel:     req.ServerModel,
			SupportCode:     req.SupportCode,
			ServerPricelist: pricelistID,
		})
		if articleErr != nil {
			return nil, articleErr
		}
		req.Article = articleResult.Article
	}

	total := req.Items.Total()
	if req.ServerCount > 1 {
		total *= float64(req.ServerCount)
	}

	// Update fields
	localCfg.Name = req.Name
	localCfg.ProjectUUID = projectUUID
	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.ServerModel = req.ServerModel
	localCfg.SupportCode = req.SupportCode
	localCfg.Article = req.Article
	localCfg.PricelistID = pricelistID
	localCfg.WarehousePricelistID = req.WarehousePricelistID
	localCfg.CompetitorPricelistID = req.CompetitorPricelistID
	localCfg.DisablePriceRefresh = req.DisablePriceRefresh
	localCfg.OnlyInStock = req.OnlyInStock
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"

	cfg, err := s.saveWithVersionAndPending(localCfg, "update", ownerUsername)
	if err != nil {
		return nil, fmt.Errorf("update configuration with version: %w", err)
	}

	return cfg, nil
}

// BuildArticlePreview generates server article based on current items and server_model/support_code.
func (s *LocalConfigurationService) BuildArticlePreview(req *ArticlePreviewRequest) (article.BuildResult, error) {
	pricelistID, err := s.resolvePricelistID(req.PricelistID)
	if err != nil {
		return article.BuildResult{}, err
	}
	return article.Build(s.localDB, req.Items, article.BuildOptions{
		ServerModel:     req.ServerModel,
		SupportCode:     req.SupportCode,
		ServerPricelist: pricelistID,
	})
}

// Delete deletes a configuration from local SQLite and queues it for sync
func (s *LocalConfigurationService) Delete(uuid string, ownerUsername string) error {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return ErrConfigNotFound
	}

	if !s.isOwner(localCfg, ownerUsername) {
		return ErrConfigForbidden
	}

	localCfg.IsActive = false
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"
	_, err = s.saveWithVersionAndPending(localCfg, "deactivate", ownerUsername)
	return err
}

// Reactivate restores an archived configuration and creates a new version.
func (s *LocalConfigurationService) Reactivate(uuid string, ownerUsername string) (*models.Configuration, error) {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}
	if !s.isOwner(localCfg, ownerUsername) {
		return nil, ErrConfigForbidden
	}
	if localCfg.IsActive {
		return localdb.LocalToConfiguration(localCfg), nil
	}

	localCfg.IsActive = true
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"
	return s.saveWithVersionAndPending(localCfg, "reactivate", ownerUsername)
}

// Rename renames a configuration
func (s *LocalConfigurationService) Rename(uuid string, ownerUsername string, newName string) (*models.Configuration, error) {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}

	if !s.isOwner(localCfg, ownerUsername) {
		return nil, ErrConfigForbidden
	}

	localCfg.Name = newName
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"

	cfg, err := s.saveWithVersionAndPending(localCfg, "update", ownerUsername)
	if err != nil {
		return nil, fmt.Errorf("rename configuration with version: %w", err)
	}
	return cfg, nil
}

// Clone clones a configuration
func (s *LocalConfigurationService) Clone(configUUID string, ownerUsername string, newName string) (*models.Configuration, error) {
	return s.CloneToProject(configUUID, ownerUsername, newName, nil)
}

func (s *LocalConfigurationService) CloneToProject(configUUID string, ownerUsername string, newName string, projectUUID *string) (*models.Configuration, error) {
	original, err := s.GetByUUID(configUUID, ownerUsername)
	if err != nil {
		return nil, err
	}
	resolvedProjectUUID := original.ProjectUUID
	if projectUUID != nil {
		resolvedProjectUUID, err = s.resolveProjectUUID(ownerUsername, projectUUID)
		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(),
		OwnerUsername:         ownerUsername,
		ProjectUUID:           resolvedProjectUUID,
		Name:                  newName,
		Items:                 original.Items,
		TotalPrice:            &total,
		CustomPrice:           original.CustomPrice,
		Notes:                 original.Notes,
		IsTemplate:            false,
		ServerCount:           original.ServerCount,
		ServerModel:           original.ServerModel,
		SupportCode:           original.SupportCode,
		Article:               original.Article,
		PricelistID:           original.PricelistID,
		WarehousePricelistID:  original.WarehousePricelistID,
		CompetitorPricelistID: original.CompetitorPricelistID,
		DisablePriceRefresh:   original.DisablePriceRefresh,
		OnlyInStock:           original.OnlyInStock,
		CreatedAt:             time.Now(),
	}

	localCfg := localdb.ConfigurationToLocal(clone)
	if err := s.createWithVersion(localCfg, ownerUsername); err != nil {
		return nil, fmt.Errorf("clone configuration with version: %w", err)
	}
	clone.Line = localCfg.Line

	return clone, nil
}

// ListByUser returns all configurations for a user from local SQLite
func (s *LocalConfigurationService) ListByUser(ownerUsername string, page, perPage int) ([]models.Configuration, int64, error) {
	return s.listByUserWithStatus(ownerUsername, page, perPage, "active")
}

func (s *LocalConfigurationService) listByUserWithStatus(ownerUsername string, page, perPage int, status string) ([]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 !matchesConfigStatus(lc.IsActive, status) {
			continue
		}
		if (lc.OriginalUsername == ownerUsername) || 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 from local cache
func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername string) (*models.Configuration, error) {
	// Get configuration from local SQLite
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}

	// Check ownership
	if !s.isOwner(localCfg, ownerUsername) {
		return nil, ErrConfigForbidden
	}

	// Refresh local pricelists when online.
	if s.isOnline() {
		if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
			slog.Warn("local configuration: background pricelist sync failed", "err", err)
		}
	}

	// Use the pricelist stored in the config; fall back to latest if unavailable.
	var pricelist *localdb.LocalPricelist
	if localCfg.PricelistID != nil && *localCfg.PricelistID > 0 {
		if pl, err := s.localDB.GetLocalPricelistByServerID(*localCfg.PricelistID); err == nil {
			pricelist = pl
		}
	}
	if pricelist == nil {
		if pl, err := s.localDB.GetLatestLocalPricelist(); err == nil {
			pricelist = pl
		}
	}

	// Capture fingerprint of the current state before any mutations.
	preRefreshFP, err := localdb.BuildConfigurationSpecPriceFingerprint(localCfg)
	if err != nil {
		return nil, fmt.Errorf("build pre-refresh fingerprint: %w", err)
	}
	preRefreshCfg := *localCfg

	// Update prices for all items from pricelist
	updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
	for i, item := range localCfg.Items {
		if pricelist != nil {
			price, err := s.localDB.GetLocalPriceForLot(pricelist.ID, item.LotName)
			if err == nil && price > 0 {
				updatedItems[i] = localdb.LocalConfigItem{
					LotName:   item.LotName,
					Quantity:  item.Quantity,
					UnitPrice: price,
				}
				continue
			}
		}

		// Keep original item if price not found in pricelist
		updatedItems[i] = item
	}

	// Update configuration
	localCfg.Items = updatedItems
	total := updatedItems.Total()

	// If server count is greater than 1, multiply the total by server count
	if localCfg.ServerCount > 1 {
		total *= float64(localCfg.ServerCount)
	}

	localCfg.TotalPrice = &total
	if pricelist != nil {
		localCfg.PricelistID = &pricelist.ServerID
	}

	// Set price update timestamp and mark for sync
	now := time.Now()
	localCfg.PriceUpdatedAt = &now
	localCfg.UpdatedAt = now
	localCfg.SyncStatus = "pending"

	// Before saving the new prices, snapshot the pre-refresh state so the revision
	// history shows a clear before/after for every price update.
	postRefreshFP, err := localdb.BuildConfigurationSpecPriceFingerprint(localCfg)
	if err != nil {
		return nil, fmt.Errorf("build post-refresh fingerprint: %w", err)
	}
	if preRefreshFP != postRefreshFP {
		if err := s.snapshotPreRefreshTx(&preRefreshCfg, ownerUsername); err != nil {
			return nil, fmt.Errorf("snapshot pre-refresh state: %w", err)
		}
	}

	cfg, err := s.saveWithVersionAndPending(localCfg, "update", ownerUsername)
	if err != nil {
		return nil, fmt.Errorf("refresh prices with version: %w", err)
	}
	return cfg, nil
}

// GetByUUIDNoAuth returns configuration without ownership check
func (s *LocalConfigurationService) GetByUUIDNoAuth(uuid string) (*models.Configuration, error) {
	var localCfg localdb.LocalConfiguration
	if err := s.localDB.DB().Preload("CurrentVersion").Where("uuid = ?", uuid).First(&localCfg).Error; err != nil {
		return nil, ErrConfigNotFound
	}
	if !localCfg.IsActive {
		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
	}

	projectUUID := localCfg.ProjectUUID
	if req.ProjectUUID != nil {
		requestedProjectUUID := strings.TrimSpace(*req.ProjectUUID)
		currentProjectUUID := ""
		if localCfg.ProjectUUID != nil {
			currentProjectUUID = strings.TrimSpace(*localCfg.ProjectUUID)
		}

		projectUUID, err = s.resolveProjectUUID(localCfg.OriginalUsername, req.ProjectUUID)
		if err != nil {
			// Allow save for legacy/orphaned configs when request keeps the same project UUID.
			// This can happen for imported configs whose project is not present in local cache.
			if errors.Is(err, ErrProjectNotFound) && requestedProjectUUID != "" && requestedProjectUUID == currentProjectUUID {
				projectUUID = localCfg.ProjectUUID
			} else {
				return nil, err
			}
		}
	}
	pricelistID, err := s.resolvePricelistID(req.PricelistID)
	if err != nil {
		return nil, err
	}

	if strings.TrimSpace(req.ServerModel) != "" {
		articleResult, articleErr := article.Build(s.localDB, req.Items, article.BuildOptions{
			ServerModel:     req.ServerModel,
			SupportCode:     req.SupportCode,
			ServerPricelist: pricelistID,
		})
		if articleErr != nil {
			return nil, articleErr
		}
		req.Article = articleResult.Article
	}

	total := req.Items.Total()
	if req.ServerCount > 1 {
		total *= float64(req.ServerCount)
	}

	localCfg.Name = req.Name
	localCfg.ProjectUUID = projectUUID
	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.ServerModel = req.ServerModel
	localCfg.SupportCode = req.SupportCode
	localCfg.Article = req.Article
	localCfg.PricelistID = pricelistID
	localCfg.WarehousePricelistID = req.WarehousePricelistID
	localCfg.CompetitorPricelistID = req.CompetitorPricelistID
	localCfg.DisablePriceRefresh = req.DisablePriceRefresh
	localCfg.OnlyInStock = req.OnlyInStock
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"

	cfg, err := s.saveWithVersionAndPending(localCfg, "update", "")
	if err != nil {
		return nil, fmt.Errorf("update configuration without auth with version: %w", err)
	}
	return cfg, nil
}

// DeleteNoAuth deletes configuration without ownership check
func (s *LocalConfigurationService) DeleteNoAuth(uuid string) error {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return ErrConfigNotFound
	}
	localCfg.IsActive = false
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"
	_, err = s.saveWithVersionAndPending(localCfg, "deactivate", "")
	return err
}

// ReactivateNoAuth restores an archived configuration without ownership check.
func (s *LocalConfigurationService) ReactivateNoAuth(uuid string) (*models.Configuration, error) {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}
	if localCfg.IsActive {
		return localdb.LocalToConfiguration(localCfg), nil
	}

	localCfg.IsActive = true
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"
	return s.saveWithVersionAndPending(localCfg, "reactivate", "")
}

// 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"

	cfg, err := s.saveWithVersionAndPending(localCfg, "update", "")
	if err != nil {
		return nil, fmt.Errorf("rename configuration without auth with version: %w", err)
	}
	return cfg, nil
}

// CloneNoAuth clones configuration without ownership check
func (s *LocalConfigurationService) CloneNoAuth(configUUID string, newName string, ownerUsername string) (*models.Configuration, error) {
	return s.CloneNoAuthToProject(configUUID, newName, ownerUsername, nil)
}

func (s *LocalConfigurationService) CloneNoAuthToProject(configUUID string, newName string, ownerUsername string, projectUUID *string) (*models.Configuration, error) {
	return s.CloneNoAuthToProjectFromVersion(configUUID, newName, ownerUsername, projectUUID, 0)
}

func (s *LocalConfigurationService) CloneNoAuthToProjectFromVersion(configUUID string, newName string, ownerUsername string, projectUUID *string, fromVersion int) (*models.Configuration, error) {
	original, err := s.GetByUUIDNoAuth(configUUID)
	if err != nil {
		return nil, err
	}

	// If fromVersion specified, use snapshot from that version
	if fromVersion > 0 {
		version, vErr := s.GetVersion(configUUID, fromVersion)
		if vErr != nil {
			return nil, vErr
		}
		snapshot, decErr := s.decodeConfigurationSnapshot(version.Data)
		if decErr != nil {
			return nil, fmt.Errorf("decode version snapshot for clone: %w", decErr)
		}
		snapshotCfg := localdb.LocalToConfiguration(snapshot)
		original = snapshotCfg
		original.UUID = configUUID // preserve original UUID for project resolution
	}

	resolvedProjectUUID := original.ProjectUUID
	if projectUUID != nil {
		resolvedProjectUUID, err = s.resolveProjectUUID(ownerUsername, projectUUID)
		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(),
		OwnerUsername:         ownerUsername,
		ProjectUUID:           resolvedProjectUUID,
		Name:                  newName,
		Items:                 original.Items,
		TotalPrice:            &total,
		CustomPrice:           original.CustomPrice,
		Notes:                 original.Notes,
		IsTemplate:            false,
		ServerCount:           original.ServerCount,
		ServerModel:           original.ServerModel,
		SupportCode:           original.SupportCode,
		Article:               original.Article,
		PricelistID:           original.PricelistID,
		WarehousePricelistID:  original.WarehousePricelistID,
		CompetitorPricelistID: original.CompetitorPricelistID,
		DisablePriceRefresh:   original.DisablePriceRefresh,
		OnlyInStock:           original.OnlyInStock,
		CreatedAt:             time.Now(),
	}

	localCfg := localdb.ConfigurationToLocal(clone)
	if err := s.createWithVersion(localCfg, ownerUsername); err != nil {
		return nil, fmt.Errorf("clone configuration without auth with version: %w", err)
	}
	clone.Line = localCfg.Line

	return clone, nil
}

// SetProjectNoAuth moves configuration to a different project without ownership check.
func (s *LocalConfigurationService) SetProjectNoAuth(uuid string, projectUUID string) (*models.Configuration, error) {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}

	var resolved *string
	trimmed := strings.TrimSpace(projectUUID)
	if trimmed == "" {
		resolved, err = s.resolveProjectUUID(localCfg.OriginalUsername, &projectUUID)
		if err != nil {
			return nil, err
		}
	} else {
		project, getErr := s.localDB.GetProjectByUUID(trimmed)
		if getErr != nil {
			return nil, ErrProjectNotFound
		}
		if !project.IsActive {
			return nil, errors.New("project is archived")
		}
		resolved = &project.UUID
	}

	localCfg.ProjectUUID = resolved
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"
	return s.saveWithVersionAndPending(localCfg, "update", "")
}

// ListAll returns all configurations without user filter
func (s *LocalConfigurationService) ListAll(page, perPage int) ([]models.Configuration, int64, error) {
	return s.ListAllWithStatus(page, perPage, "active", "")
}

// ListAllWithStatus returns configurations filtered by status: active|archived|all.
func (s *LocalConfigurationService) ListAllWithStatus(page, perPage int, status string, search string) ([]models.Configuration, int64, error) {
	// Apply pagination
	if page < 1 {
		page = 1
	}
	if perPage < 1 || perPage > 100 {
		perPage = 20
	}
	offset := (page - 1) * perPage
	localConfigs, total, err := s.localDB.ListConfigurationsWithFilters(status, search, offset, perPage)
	if err != nil {
		return nil, 0, err
	}
	configs := make([]models.Configuration, 0, len(localConfigs))
	for _, lc := range localConfigs {
		configs = append(configs, *localdb.LocalToConfiguration(&lc))
	}
	return configs, 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.IsActive {
			continue
		}
		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.
// pricelistServerID optionally specifies which pricelist to use; if nil, the config's stored
// pricelist is used; if that is also absent, the latest local pricelist is used as a fallback.
func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string, pricelistServerID *uint) (*models.Configuration, error) {
	// Get configuration from local SQLite
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}

	if s.isOnline() {
		if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
			slog.Warn("local configuration: background pricelist sync failed", "err", err)
		}
	}

	// Resolve which pricelist to use:
	// 1. Explicitly requested pricelist (from UI selection)
	// 2. Pricelist stored in the configuration
	// 3. Latest local pricelist as last-resort fallback
	var targetServerID *uint
	if pricelistServerID != nil && *pricelistServerID > 0 {
		targetServerID = pricelistServerID
	} else if localCfg.PricelistID != nil && *localCfg.PricelistID > 0 {
		targetServerID = localCfg.PricelistID
	}

	var pricelist *localdb.LocalPricelist
	if targetServerID != nil {
		if pl, err := s.localDB.GetLocalPricelistByServerID(*targetServerID); err == nil {
			pricelist = pl
		}
	}
	if pricelist == nil {
		// Fallback: use latest local pricelist
		if pl, err := s.localDB.GetLatestLocalPricelist(); err == nil {
			pricelist = pl
		}
	}

	// Capture fingerprint of the current state before any mutations.
	preRefreshFP, err := localdb.BuildConfigurationSpecPriceFingerprint(localCfg)
	if err != nil {
		return nil, fmt.Errorf("build pre-refresh fingerprint: %w", err)
	}
	preRefreshCfg := *localCfg

	// Update prices for all items from pricelist
	updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
	for i, item := range localCfg.Items {
		if pricelist != nil {
			price, err := s.localDB.GetLocalPriceForLot(pricelist.ID, item.LotName)
			if err == nil && price > 0 {
				updatedItems[i] = localdb.LocalConfigItem{
					LotName:   item.LotName,
					Quantity:  item.Quantity,
					UnitPrice: price,
				}
				continue
			}
		}

		// Keep original item if price not found in pricelist
		updatedItems[i] = item
	}

	// Update configuration
	localCfg.Items = updatedItems
	total := updatedItems.Total()

	// If server count is greater than 1, multiply the total by server count
	if localCfg.ServerCount > 1 {
		total *= float64(localCfg.ServerCount)
	}

	localCfg.TotalPrice = &total
	if pricelist != nil {
		localCfg.PricelistID = &pricelist.ServerID
	}

	// Set price update timestamp and mark for sync
	now := time.Now()
	localCfg.PriceUpdatedAt = &now
	localCfg.UpdatedAt = now
	localCfg.SyncStatus = "pending"

	// Before saving the new prices, snapshot the pre-refresh state so the revision
	// history shows a clear before/after for every price update.
	postRefreshFP, err := localdb.BuildConfigurationSpecPriceFingerprint(localCfg)
	if err != nil {
		return nil, fmt.Errorf("build post-refresh fingerprint: %w", err)
	}
	if preRefreshFP != postRefreshFP {
		if err := s.snapshotPreRefreshTx(&preRefreshCfg, ""); err != nil {
			return nil, fmt.Errorf("snapshot pre-refresh state: %w", err)
		}
	}

	cfg, err := s.saveWithVersionAndPending(localCfg, "update", "")
	if err != nil {
		return nil, fmt.Errorf("refresh prices without auth with version: %w", err)
	}
	return cfg, nil
}

// SnapshotCurrentState creates a revision of the current configuration state without modifying it.
// Called before a client-side price refresh so the revision history has a clear before/after.
func (s *LocalConfigurationService) SnapshotCurrentState(uuid string) error {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return ErrConfigNotFound
	}
	return s.snapshotPreRefreshTx(localCfg, "")
}

// UpdateServerCount updates server count and recalculates total price without creating a new version.
func (s *LocalConfigurationService) UpdateServerCount(configUUID string, serverCount int) (*models.Configuration, error) {
	if serverCount < 1 {
		return nil, fmt.Errorf("server count must be at least 1")
	}

	localCfg, err := s.localDB.GetConfigurationByUUID(configUUID)
	if err != nil {
		return nil, ErrConfigNotFound
	}

	localCfg.ServerCount = serverCount
	total := localCfg.Items.Total()
	if serverCount > 1 {
		total *= float64(serverCount)
	}
	localCfg.TotalPrice = &total
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"

	var cfg *models.Configuration
	err = s.localDB.DB().Transaction(func(tx *gorm.DB) error {
		if err := tx.Save(localCfg).Error; err != nil {
			return fmt.Errorf("save local configuration: %w", err)
		}

		version, err := s.loadVersionForPendingTx(tx, localCfg)
		if err != nil {
			return err
		}

		cfg = localdb.LocalToConfiguration(localCfg)
		if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, "update", version, ""); err != nil {
			return fmt.Errorf("enqueue server-count pending change: %w", err)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	return cfg, nil
}

func (s *LocalConfigurationService) ReorderProjectConfigurationsNoAuth(projectUUID string, orderedUUIDs []string) ([]models.Configuration, error) {
	projectUUID = strings.TrimSpace(projectUUID)
	if projectUUID == "" {
		return nil, ErrProjectNotFound
	}
	if _, err := s.localDB.GetProjectByUUID(projectUUID); err != nil {
		return nil, ErrProjectNotFound
	}
	if len(orderedUUIDs) == 0 {
		return []models.Configuration{}, nil
	}

	seen := make(map[string]struct{}, len(orderedUUIDs))
	normalized := make([]string, 0, len(orderedUUIDs))
	for _, raw := range orderedUUIDs {
		u := strings.TrimSpace(raw)
		if u == "" {
			return nil, fmt.Errorf("ordered_uuids contains empty uuid")
		}
		if _, exists := seen[u]; exists {
			return nil, fmt.Errorf("ordered_uuids contains duplicate uuid: %s", u)
		}
		seen[u] = struct{}{}
		normalized = append(normalized, u)
	}

	err := s.localDB.DB().Transaction(func(tx *gorm.DB) error {
		var active []localdb.LocalConfiguration
		if err := tx.Where("project_uuid = ? AND is_active = ?", projectUUID, true).
			Find(&active).Error; err != nil {
			return fmt.Errorf("load project active configurations: %w", err)
		}
		if len(active) != len(normalized) {
			return fmt.Errorf("ordered_uuids count mismatch: expected %d got %d", len(active), len(normalized))
		}

		byUUID := make(map[string]*localdb.LocalConfiguration, len(active))
		for i := range active {
			cfg := active[i]
			byUUID[cfg.UUID] = &cfg
		}
		for _, id := range normalized {
			if _, ok := byUUID[id]; !ok {
				return fmt.Errorf("configuration %s not found in project %s", id, projectUUID)
			}
		}

		now := time.Now()
		for idx, id := range normalized {
			cfg := byUUID[id]
			newLine := (idx + 1) * 10
			if cfg.Line == newLine {
				continue
			}

			cfg.Line = newLine
			cfg.UpdatedAt = now
			cfg.SyncStatus = "pending"

			if err := tx.Save(cfg).Error; err != nil {
				return fmt.Errorf("save reordered configuration %s: %w", cfg.UUID, err)
			}

			version, err := s.loadVersionForPendingTx(tx, cfg)
			if err != nil {
				return err
			}
			if err := s.enqueueConfigurationPendingChangeTx(tx, cfg, "update", version, ""); err != nil {
				return fmt.Errorf("enqueue reorder pending change for %s: %w", cfg.UUID, err)
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	var localConfigs []localdb.LocalConfiguration
	if err := s.localDB.DB().
		Preload("CurrentVersion").
		Where("project_uuid = ? AND is_active = ?", projectUUID, true).
		Order("CASE WHEN COALESCE(line_no, 0) <= 0 THEN 2147483647 ELSE line_no END ASC, created_at DESC, id DESC").
		Find(&localConfigs).Error; err != nil {
		return nil, fmt.Errorf("load reordered configurations: %w", err)
	}

	result := make([]models.Configuration, 0, len(localConfigs))
	for i := range localConfigs {
		result = append(result, *localdb.LocalToConfiguration(&localConfigs[i]))
	}
	return result, nil
}

// ImportFromServer imports configurations from MariaDB to local SQLite cache.
func (s *LocalConfigurationService) ImportFromServer() (*sync.ConfigImportResult, error) {
	return s.syncService.ImportConfigurationsToLocal()
}

// GetCurrentVersion returns the currently active version row for configuration UUID.
func (s *LocalConfigurationService) GetCurrentVersion(configurationUUID string) (*localdb.LocalConfigurationVersion, error) {
	var cfg localdb.LocalConfiguration
	if err := s.localDB.DB().Where("uuid = ?", configurationUUID).First(&cfg).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrConfigNotFound
		}
		return nil, fmt.Errorf("get configuration for current version: %w", err)
	}

	var version localdb.LocalConfigurationVersion
	if cfg.CurrentVersionID != nil && *cfg.CurrentVersionID != "" {
		if err := s.localDB.DB().
			Where("id = ? AND configuration_uuid = ?", *cfg.CurrentVersionID, configurationUUID).
			First(&version).Error; err == nil {
			return &version, nil
		}
	}

	if err := s.localDB.DB().
		Where("configuration_uuid = ?", configurationUUID).
		Order("version_no DESC").
		First(&version).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrConfigVersionNotFound
		}
		return nil, fmt.Errorf("get latest version for current pointer fallback: %w", err)
	}

	return &version, nil
}

// ListVersions returns versions by configuration UUID in descending order by version number.
func (s *LocalConfigurationService) ListVersions(configurationUUID string, limit, offset int) ([]localdb.LocalConfigurationVersion, error) {
	if limit <= 0 {
		limit = 20
	}
	if limit > 200 {
		limit = 200
	}
	if offset < 0 {
		return nil, ErrInvalidVersionNumber
	}

	var cfgCount int64
	if err := s.localDB.DB().Model(&localdb.LocalConfiguration{}).
		Where("uuid = ?", configurationUUID).
		Count(&cfgCount).Error; err != nil {
		return nil, fmt.Errorf("check configuration before list versions: %w", err)
	}
	if cfgCount == 0 {
		return nil, ErrConfigNotFound
	}

	var versions []localdb.LocalConfigurationVersion
	if err := s.localDB.DB().
		Where("configuration_uuid = ?", configurationUUID).
		Order("version_no DESC").
		Limit(limit).
		Offset(offset).
		Find(&versions).Error; err != nil {
		return nil, fmt.Errorf("list versions: %w", err)
	}

	return versions, nil
}

// GetVersion returns one version by configuration UUID and version number.
func (s *LocalConfigurationService) GetVersion(configurationUUID string, versionNo int) (*localdb.LocalConfigurationVersion, error) {
	if versionNo <= 0 {
		return nil, ErrInvalidVersionNumber
	}

	var version localdb.LocalConfigurationVersion
	if err := s.localDB.DB().
		Where("configuration_uuid = ? AND version_no = ?", configurationUUID, versionNo).
		First(&version).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrConfigVersionNotFound
		}
		return nil, fmt.Errorf("get version %d for %s: %w", versionNo, configurationUUID, err)
	}

	return &version, nil
}

// RollbackToVersion creates a new version from target snapshot and marks it current.
func (s *LocalConfigurationService) RollbackToVersion(configurationUUID string, targetVersionNo int, userID string) (*models.Configuration, error) {
	return s.rollbackToVersion(configurationUUID, targetVersionNo, userID, "")
}

// RollbackToVersionWithNote same as RollbackToVersion, with optional user note.
func (s *LocalConfigurationService) RollbackToVersionWithNote(configurationUUID string, targetVersionNo int, userID string, note string) (*models.Configuration, error) {
	return s.rollbackToVersion(configurationUUID, targetVersionNo, userID, note)
}

func (s *LocalConfigurationService) isOwner(cfg *localdb.LocalConfiguration, ownerUsername string) bool {
	if cfg == nil || ownerUsername == "" {
		return false
	}
	if cfg.OriginalUsername != "" {
		return cfg.OriginalUsername == ownerUsername
	}
	return false
}

func (s *LocalConfigurationService) createWithVersion(localCfg *localdb.LocalConfiguration, createdBy string) error {
	return s.localDB.DB().Transaction(func(tx *gorm.DB) error {
		return s.createWithVersionTx(tx, localCfg, createdBy)
	})
}

func (s *LocalConfigurationService) createWithVersionTx(tx *gorm.DB, localCfg *localdb.LocalConfiguration, createdBy string) error {
	if localCfg.IsActive {
		if err := s.ensureConfigurationLineTx(tx, localCfg); err != nil {
			return err
		}
	}
	if err := tx.Create(localCfg).Error; err != nil {
		return fmt.Errorf("create local configuration: %w", err)
	}

	version, err := s.appendVersionTx(tx, localCfg, "create", createdBy)
	if err != nil {
		return fmt.Errorf("append create version: %w", err)
	}

	if err := tx.Model(&localdb.LocalConfiguration{}).
		Where("uuid = ?", localCfg.UUID).
		Update("current_version_id", version.ID).Error; err != nil {
		return fmt.Errorf("set current version id: %w", err)
	}
	localCfg.CurrentVersionID = &version.ID
	localCfg.CurrentVersion = version

	if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, "create", version, createdBy); err != nil {
		return fmt.Errorf("enqueue create pending change: %w", err)
	}
	if err := s.recalculateLocalPricelistUsageTx(tx); err != nil {
		return fmt.Errorf("recalculate local pricelist usage: %w", err)
	}

	return nil
}

func (s *LocalConfigurationService) saveWithVersionAndPending(localCfg *localdb.LocalConfiguration, operation string, createdBy string) (*models.Configuration, error) {
	var cfg *models.Configuration

	err := s.localDB.DB().Transaction(func(tx *gorm.DB) error {
		var locked localdb.LocalConfiguration
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("uuid = ?", localCfg.UUID).
			First(&locked).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ErrConfigNotFound
			}
			return fmt.Errorf("lock configuration row: %w", err)
		}

		if operation == "update" {
			currentVersion, err := s.loadCurrentVersionTx(tx, &locked)
			if err != nil {
				return fmt.Errorf("load current version before save: %w", err)
			}

			// Legacy/orphaned rows may have empty or stale current_version_id.
			// In that case we treat update as content-changing and append a fresh version.
			if currentVersion != nil {
				sameRevisionContent, err := s.hasSameRevisionContent(localCfg, currentVersion)
				if err != nil {
					return fmt.Errorf("compare revision content: %w", err)
				}
				if sameRevisionContent {
					if !hasNonRevisionConfigurationChanges(&locked, localCfg) {
						cfg = localdb.LocalToConfiguration(&locked)
						return nil
					}
					if err := tx.Save(localCfg).Error; err != nil {
						return fmt.Errorf("save local configuration (no new revision): %w", err)
					}
					cfg = localdb.LocalToConfiguration(localCfg)
					if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, operation, currentVersion, createdBy); err != nil {
						return fmt.Errorf("enqueue %s pending change without revision: %w", operation, err)
					}
					if err := s.recalculateLocalPricelistUsageTx(tx); err != nil {
						return fmt.Errorf("recalculate local pricelist usage: %w", err)
					}
					return nil
				}
			}
		}

		if localCfg.IsActive {
			if err := s.ensureConfigurationLineTx(tx, localCfg); err != nil {
				return err
			}
		}

		if err := tx.Save(localCfg).Error; err != nil {
			return fmt.Errorf("save local configuration: %w", err)
		}

		version, err := s.appendVersionTx(tx, localCfg, operation, createdBy)
		if err != nil {
			return fmt.Errorf("append %s version: %w", operation, err)
		}

		if err := tx.Model(&localdb.LocalConfiguration{}).
			Where("uuid = ?", localCfg.UUID).
			Update("current_version_id", version.ID).Error; err != nil {
			return fmt.Errorf("update current version id: %w", err)
		}
		localCfg.CurrentVersionID = &version.ID
		localCfg.CurrentVersion = version

		cfg = localdb.LocalToConfiguration(localCfg)
		if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, operation, version, createdBy); err != nil {
			return fmt.Errorf("enqueue %s pending change: %w", operation, err)
		}
		if err := s.recalculateLocalPricelistUsageTx(tx); err != nil {
			return fmt.Errorf("recalculate local pricelist usage: %w", err)
		}

		return nil
	})
	if err != nil {
		return nil, err
	}

	return cfg, nil
}

func hasNonRevisionConfigurationChanges(current *localdb.LocalConfiguration, next *localdb.LocalConfiguration) bool {
	if current == nil || next == nil {
		return true
	}
	if current.Name != next.Name ||
		current.Notes != next.Notes ||
		current.IsTemplate != next.IsTemplate ||
		current.ServerModel != next.ServerModel ||
		current.SupportCode != next.SupportCode ||
		current.Article != next.Article ||
		current.IsActive != next.IsActive ||
		current.Line != next.Line {
		return true
	}
	if !equalStringPtr(current.ProjectUUID, next.ProjectUUID) {
		return true
	}
	return false
}

func (s *LocalConfigurationService) UpdateVendorSpecNoAuth(uuid string, spec localdb.VendorSpec) (*models.Configuration, error) {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}

	localCfg.VendorSpec = spec
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"

	cfg, err := s.saveWithVersionAndPending(localCfg, "update", "")
	if err != nil {
		return nil, fmt.Errorf("update vendor spec without auth with version: %w", err)
	}
	return cfg, nil
}

func (s *LocalConfigurationService) ApplyVendorSpecItemsNoAuth(uuid string, items localdb.LocalConfigItems) (*models.Configuration, error) {
	localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, ErrConfigNotFound
	}

	localCfg.Items = items
	total := items.Total()
	if localCfg.ServerCount > 1 {
		total *= float64(localCfg.ServerCount)
	}
	localCfg.TotalPrice = &total
	localCfg.UpdatedAt = time.Now()
	localCfg.SyncStatus = "pending"

	cfg, err := s.saveWithVersionAndPending(localCfg, "update", "")
	if err != nil {
		return nil, fmt.Errorf("apply vendor spec items without auth with version: %w", err)
	}
	return cfg, nil
}

func equalStringPtr(a, b *string) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return strings.TrimSpace(*a) == strings.TrimSpace(*b)
}

func equalUintPtr(a, b *uint) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

func (s *LocalConfigurationService) loadCurrentVersionTx(tx *gorm.DB, localCfg *localdb.LocalConfiguration) (*localdb.LocalConfigurationVersion, error) {
	var version localdb.LocalConfigurationVersion
	if localCfg.CurrentVersionID != nil && *localCfg.CurrentVersionID != "" {
		if err := tx.Where("id = ?", *localCfg.CurrentVersionID).First(&version).Error; err == nil {
			return &version, nil
		} else if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}

	if err := tx.Where("configuration_uuid = ?", localCfg.UUID).
		Order("version_no DESC").
		First(&version).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return &version, nil
}

func (s *LocalConfigurationService) loadVersionForPendingTx(tx *gorm.DB, localCfg *localdb.LocalConfiguration) (*localdb.LocalConfigurationVersion, error) {
	if localCfg.CurrentVersionID != nil && *localCfg.CurrentVersionID != "" {
		var current localdb.LocalConfigurationVersion
		if err := tx.Where("id = ?", *localCfg.CurrentVersionID).First(&current).Error; err == nil {
			return &current, nil
		}
	}

	var latest localdb.LocalConfigurationVersion
	if err := tx.Where("configuration_uuid = ?", localCfg.UUID).
		Order("version_no DESC").
		First(&latest).Error; err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fmt.Errorf("load version for pending change: %w", err)
		}

		// Legacy/imported rows may exist without local version history.
		// Bootstrap the first version so pending sync payloads can reference a version.
		version, createErr := s.appendVersionTx(tx, localCfg, "bootstrap", "")
		if createErr != nil {
			return nil, fmt.Errorf("bootstrap version for pending change: %w", createErr)
		}
		if err := tx.Model(&localdb.LocalConfiguration{}).
			Where("uuid = ?", localCfg.UUID).
			Update("current_version_id", version.ID).Error; err != nil {
			return nil, fmt.Errorf("set current version id for bootstrapped pending change: %w", err)
		}
		localCfg.CurrentVersionID = &version.ID
		return version, nil
	}
	return &latest, nil
}

func (s *LocalConfigurationService) ensureConfigurationLineTx(tx *gorm.DB, localCfg *localdb.LocalConfiguration) error {
	if localCfg == nil || !localCfg.IsActive {
		return nil
	}

	needsAssign := localCfg.Line <= 0
	if !needsAssign {
		query := tx.Model(&localdb.LocalConfiguration{}).
			Where("is_active = ? AND line_no = ?", true, localCfg.Line)

		if strings.TrimSpace(localCfg.UUID) != "" {
			query = query.Where("uuid <> ?", strings.TrimSpace(localCfg.UUID))
		}

		if localCfg.ProjectUUID != nil && strings.TrimSpace(*localCfg.ProjectUUID) != "" {
			query = query.Where("project_uuid = ?", strings.TrimSpace(*localCfg.ProjectUUID))
		} else {
			query = query.Where("project_uuid IS NULL OR TRIM(project_uuid) = ''")
		}

		var conflicts int64
		if err := query.Count(&conflicts).Error; err != nil {
			return fmt.Errorf("check line_no conflict for configuration %s: %w", localCfg.UUID, err)
		}
		needsAssign = conflicts > 0
	}

	if needsAssign {
		line, err := localdb.NextConfigurationLineTx(tx, localCfg.ProjectUUID, localCfg.UUID)
		if err != nil {
			return fmt.Errorf("assign line_no for configuration %s: %w", localCfg.UUID, err)
		}
		localCfg.Line = line
	}
	return nil
}

func (s *LocalConfigurationService) hasSameRevisionContent(localCfg *localdb.LocalConfiguration, currentVersion *localdb.LocalConfigurationVersion) (bool, error) {
	currentSnapshotCfg, err := s.decodeConfigurationSnapshot(currentVersion.Data)
	if err != nil {
		return false, fmt.Errorf("decode current version snapshot: %w", err)
	}

	currentFingerprint, err := localdb.BuildConfigurationSpecPriceFingerprint(currentSnapshotCfg)
	if err != nil {
		return false, fmt.Errorf("build current snapshot fingerprint: %w", err)
	}
	nextFingerprint, err := localdb.BuildConfigurationSpecPriceFingerprint(localCfg)
	if err != nil {
		return false, fmt.Errorf("build next snapshot fingerprint: %w", err)
	}
	return currentFingerprint == nextFingerprint, nil
}

func (s *LocalConfigurationService) appendVersionTx(
	tx *gorm.DB,
	localCfg *localdb.LocalConfiguration,
	operation string,
	createdBy string,
) (*localdb.LocalConfigurationVersion, error) {
	return s.appendVersionTxNote(tx, localCfg, operation, createdBy, "")
}

func (s *LocalConfigurationService) appendVersionTxNote(
	tx *gorm.DB,
	localCfg *localdb.LocalConfiguration,
	operation string,
	createdBy string,
	noteOverride string,
) (*localdb.LocalConfigurationVersion, error) {
	snapshot, err := s.buildConfigurationSnapshot(localCfg)
	if err != nil {
		return nil, fmt.Errorf("build snapshot: %w", err)
	}
	changeNote := fmt.Sprintf("%s via local-first flow", operation)
	if noteOverride != "" {
		changeNote = noteOverride
	}

	var createdByPtr *string
	if createdBy != "" {
		createdByPtr = &createdBy
	}

	for attempt := 0; attempt < 3; attempt++ {
		var maxVersion int
		if err := tx.Model(&localdb.LocalConfigurationVersion{}).
			Where("configuration_uuid = ?", localCfg.UUID).
			Select("COALESCE(MAX(version_no), 0)").
			Scan(&maxVersion).Error; err != nil {
			return nil, fmt.Errorf("read max version: %w", err)
		}

		versionID := uuid.New().String()
		version := &localdb.LocalConfigurationVersion{
			ID:                versionID,
			ConfigurationUUID: localCfg.UUID,
			VersionNo:         maxVersion + 1,
			Data:              snapshot,
			ChangeNote:        &changeNote,
			CreatedBy:         createdByPtr,
			AppVersion:        appmeta.Version(),
		}

		if err := tx.Create(version).Error; err != nil {
			// SQLite equivalent safety: serialized writer tx + UNIQUE(configuration_uuid, version_no) + retry.
			if strings.Contains(err.Error(), "UNIQUE constraint failed: local_configuration_versions.configuration_uuid, local_configuration_versions.version_no") {
				continue
			}
			return nil, fmt.Errorf("insert configuration version: %w", err)
		}

		return version, nil
	}

	return nil, fmt.Errorf("%w: exceeded retries for %s", ErrVersionConflict, localCfg.UUID)
}

// snapshotPreRefreshTx creates a revision of the current configuration state before a price
// refresh so the history clearly shows what existed before prices were updated.
// Called only when prices are about to change (fingerprints differ).
func (s *LocalConfigurationService) snapshotPreRefreshTx(localCfg *localdb.LocalConfiguration, createdBy string) error {
	return s.localDB.DB().Transaction(func(tx *gorm.DB) error {
		var locked localdb.LocalConfiguration
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("uuid = ?", localCfg.UUID).
			First(&locked).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ErrConfigNotFound
			}
			return fmt.Errorf("lock row for pre-refresh snapshot: %w", err)
		}

		version, err := s.appendVersionTxNote(tx, localCfg, "update", createdBy, "до обновления цен")
		if err != nil {
			return fmt.Errorf("append pre-refresh version: %w", err)
		}

		if err := tx.Model(&localdb.LocalConfiguration{}).
			Where("uuid = ?", localCfg.UUID).
			Update("current_version_id", version.ID).Error; err != nil {
			return fmt.Errorf("set current_version_id for pre-refresh snapshot: %w", err)
		}
		return nil
	})
}

func (s *LocalConfigurationService) buildConfigurationSnapshot(localCfg *localdb.LocalConfiguration) (string, error) {
	return localdb.BuildConfigurationSnapshot(localCfg)
}

func (s *LocalConfigurationService) rollbackToVersion(configurationUUID string, targetVersionNo int, userID string, note string) (*models.Configuration, error) {
	if targetVersionNo <= 0 {
		return nil, ErrInvalidVersionNumber
	}

	var resultCfg *models.Configuration
	err := s.localDB.DB().Transaction(func(tx *gorm.DB) error {
		var current localdb.LocalConfiguration
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("uuid = ?", configurationUUID).
			First(&current).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ErrConfigNotFound
			}
			return fmt.Errorf("lock configuration for rollback: %w", err)
		}

		var target localdb.LocalConfigurationVersion
		if err := tx.Where("configuration_uuid = ? AND version_no = ?", configurationUUID, targetVersionNo).
			First(&target).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ErrConfigVersionNotFound
			}
			return fmt.Errorf("load target rollback version: %w", err)
		}

		rollbackData, err := s.decodeConfigurationSnapshot(target.Data)
		if err != nil {
			return fmt.Errorf("decode target rollback snapshot: %w", err)
		}

		// Keep stable identity/sync linkage; restore editable config content from target snapshot.
		current.Name = rollbackData.Name
		current.Items = rollbackData.Items
		current.TotalPrice = rollbackData.TotalPrice
		current.CustomPrice = rollbackData.CustomPrice
		current.Notes = rollbackData.Notes
		current.IsTemplate = rollbackData.IsTemplate
		current.ServerCount = rollbackData.ServerCount
		current.ServerModel = rollbackData.ServerModel
		current.SupportCode = rollbackData.SupportCode
		current.Article = rollbackData.Article
		current.PricelistID = rollbackData.PricelistID
		current.WarehousePricelistID = rollbackData.WarehousePricelistID
		current.CompetitorPricelistID = rollbackData.CompetitorPricelistID
		current.DisablePriceRefresh = rollbackData.DisablePriceRefresh
		current.OnlyInStock = rollbackData.OnlyInStock
		current.VendorSpec = rollbackData.VendorSpec
		if rollbackData.Line > 0 {
			current.Line = rollbackData.Line
		}
		current.PriceUpdatedAt = rollbackData.PriceUpdatedAt
		current.UpdatedAt = time.Now()
		current.SyncStatus = "pending"
		current.IsActive = rollbackData.IsActive

		if rollbackData.OriginalUsername != "" {
			current.OriginalUsername = rollbackData.OriginalUsername
		}
		if rollbackData.OriginalUserID != 0 {
			current.OriginalUserID = rollbackData.OriginalUserID
		}

		if err := tx.Save(&current).Error; err != nil {
			return fmt.Errorf("save rolled back configuration: %w", err)
		}

		var maxVersion int
		if err := tx.Model(&localdb.LocalConfigurationVersion{}).
			Where("configuration_uuid = ?", configurationUUID).
			Select("COALESCE(MAX(version_no), 0)").
			Scan(&maxVersion).Error; err != nil {
			return fmt.Errorf("read max version before rollback append: %w", err)
		}

		changeNote := fmt.Sprintf("rollback to v%d", targetVersionNo)
		if trimmed := strings.TrimSpace(note); trimmed != "" {
			changeNote = fmt.Sprintf("%s (%s)", changeNote, trimmed)
		}

		version := &localdb.LocalConfigurationVersion{
			ID:                uuid.New().String(),
			ConfigurationUUID: configurationUUID,
			VersionNo:         maxVersion + 1,
			Data:              target.Data,
			ChangeNote:        &changeNote,
			CreatedBy:         stringPtrOrNil(userID),
			AppVersion:        appmeta.Version(),
		}

		if err := tx.Create(version).Error; err != nil {
			if strings.Contains(err.Error(), "UNIQUE constraint failed: local_configuration_versions.configuration_uuid, local_configuration_versions.version_no") {
				return ErrVersionConflict
			}
			return fmt.Errorf("create rollback version: %w", err)
		}

		if err := tx.Model(&localdb.LocalConfiguration{}).
			Where("uuid = ?", configurationUUID).
			Update("current_version_id", version.ID).Error; err != nil {
			return fmt.Errorf("update current version after rollback: %w", err)
		}
		current.CurrentVersionID = &version.ID

		resultCfg = localdb.LocalToConfiguration(&current)
		if err := s.enqueueConfigurationPendingChangeTx(tx, &current, "rollback", version, userID); err != nil {
			return fmt.Errorf("enqueue rollback pending change: %w", err)
		}
		if err := s.recalculateLocalPricelistUsageTx(tx); err != nil {
			return fmt.Errorf("recalculate local pricelist usage: %w", err)
		}

		return nil
	})
	if err != nil {
		return nil, err
	}

	return resultCfg, nil
}

func (s *LocalConfigurationService) enqueueConfigurationPendingChangeTx(
	tx *gorm.DB,
	localCfg *localdb.LocalConfiguration,
	operation string,
	version *localdb.LocalConfigurationVersion,
	createdBy string,
) error {
	cfg := localdb.LocalToConfiguration(localCfg)
	payload := sync.ConfigurationChangePayload{
		EventID:           uuid.New().String(),
		IdempotencyKey:    fmt.Sprintf("%s:v%d:%s", localCfg.UUID, version.VersionNo, operation),
		ConfigurationUUID: localCfg.UUID,
		ProjectUUID:       localCfg.ProjectUUID,
		PricelistID:       localCfg.PricelistID,
		Operation:         operation,
		CurrentVersionID:  version.ID,
		CurrentVersionNo:  version.VersionNo,
		ConflictPolicy:    "last_write_wins",
		Snapshot:          *cfg,
		CreatedAt:         time.Now().UTC(),
		CreatedBy:         stringPtrOrNil(createdBy),
	}

	rawPayload, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal pending payload: %w", err)
	}

	change := &localdb.PendingChange{
		EntityType: "configuration",
		EntityUUID: localCfg.UUID,
		Operation:  operation,
		Payload:    string(rawPayload),
		CreatedAt:  time.Now(),
		Attempts:   0,
	}
	if err := tx.Create(change).Error; err != nil {
		return fmt.Errorf("insert pending change: %w", err)
	}

	return nil
}

func (s *LocalConfigurationService) decodeConfigurationSnapshot(data string) (*localdb.LocalConfiguration, error) {
	return localdb.DecodeConfigurationSnapshot(data)
}

func (s *LocalConfigurationService) recalculateLocalPricelistUsageTx(tx *gorm.DB) error {
	if err := tx.Model(&localdb.LocalPricelist{}).Where("1 = 1").Update("is_used", false).Error; err != nil {
		return err
	}
	return tx.Exec(`
		UPDATE local_pricelists
		SET is_used = 1
		WHERE server_id IN (
			SELECT DISTINCT pricelist_id
			FROM local_configurations
			WHERE pricelist_id IS NOT NULL AND is_active = 1
		)
	`).Error
}

func stringPtrOrNil(value string) *string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return nil
	}
	return &trimmed
}

func matchesConfigStatus(isActive bool, status string) bool {
	switch status {
	case "active", "":
		return isActive
	case "archived":
		return !isActive
	case "all":
		return true
	default:
		return isActive
	}
}

func (s *LocalConfigurationService) resolveProjectUUID(ownerUsername string, projectUUID *string) (*string, error) {
	if ownerUsername == "" {
		ownerUsername = s.localDB.GetDBUser()
	}

	if projectUUID == nil || strings.TrimSpace(*projectUUID) == "" {
		project, err := s.localDB.EnsureDefaultProject(ownerUsername)
		if err != nil {
			return nil, err
		}
		return &project.UUID, nil
	}

	requested := strings.TrimSpace(*projectUUID)
	project, err := s.localDB.GetProjectByUUID(requested)
	if err != nil {
		return nil, ErrProjectNotFound
	}
	if !project.IsActive {
		return nil, errors.New("project is archived")
	}

	return &project.UUID, nil
}

func (s *LocalConfigurationService) resolvePricelistID(pricelistID *uint) (*uint, error) {
	if pricelistID != nil && *pricelistID > 0 {
		if _, err := s.localDB.GetLocalPricelistByServerID(*pricelistID); err == nil {
			return pricelistID, nil
		}
		if s.isOnline() {
			if _, err := s.syncService.SyncPricelists(); err == nil {
				if _, err := s.localDB.GetLocalPricelistByServerID(*pricelistID); err == nil {
					return pricelistID, nil
				}
			}
		}
		return nil, fmt.Errorf("pricelist %d not available locally", *pricelistID)
	}

	latest, err := s.localDB.GetLatestLocalPricelist()
	if err != nil {
		return nil, nil
	}
	return &latest.ServerID, nil
}
