package gormstore

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/lukaszraczylo/gohoarder/pkg/errors"
	"github.com/lukaszraczylo/gohoarder/pkg/metadata"
	"github.com/rs/zerolog/log"
	"gorm.io/driver/mysql"
	"gorm.io/driver/postgres"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// GORMStoreV2 implements metadata.Store interface with optimized V2 schema
type GORMStoreV2 struct {
	db                *gorm.DB
	registryCache     map[string]int32 // Cache registry name -> ID mapping
	aggregationWorker *AggregationWorker
	partitionManager  *PartitionManager
	registryCacheMu   sync.RWMutex // Protects registryCache for concurrent access
}

// NewV2 creates a new GORM-based metadata store with V2 schema
func NewV2(cfg Config) (*GORMStoreV2, error) {
	if err := cfg.Validate(); err != nil {
		return nil, err
	}

	// Configure GORM logger
	var gormLogger logger.Interface
	switch cfg.LogLevel {
	case "silent":
		gormLogger = logger.Default.LogMode(logger.Silent)
	case "error":
		gormLogger = logger.Default.LogMode(logger.Error)
	case "warn":
		gormLogger = logger.Default.LogMode(logger.Warn)
	case "info":
		gormLogger = logger.Default.LogMode(logger.Info)
	default:
		gormLogger = logger.Default.LogMode(logger.Warn)
	}

	// Initialize database connection
	var dialector gorm.Dialector
	switch cfg.Driver {
	case "sqlite":
		dialector = sqlite.Open(cfg.DSN)
	case "postgres", "postgresql":
		dialector = postgres.Open(cfg.DSN)
	case "mysql":
		dialector = mysql.Open(cfg.DSN)
	default:
		return nil, errors.New(errors.ErrCodeInvalidConfig, "unsupported driver: "+cfg.Driver)
	}

	db, err := gorm.Open(dialector, &gorm.Config{
		Logger:                 gormLogger,
		SkipDefaultTransaction: true, // Better performance
		PrepareStmt:            true, // Cached prepared statements
	})
	if err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to connect to database")
	}

	// Configure connection pool
	sqlDB, err := db.DB()
	if err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to get sql.DB")
	}

	sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
	sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
	sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)

	// Auto-migrate schema
	if err := db.AutoMigrate(GetAllModels()...); err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to migrate database")
	}

	store := &GORMStoreV2{
		db:            db,
		registryCache: make(map[string]int32),
	}

	// Initialize partition manager (PostgreSQL only)
	if cfg.Driver == "postgres" || cfg.Driver == "postgresql" {
		store.partitionManager = NewPartitionManager(db)
		if err := store.partitionManager.EnsurePartitions(); err != nil {
			log.Warn().Err(err).Msg("Failed to create partitions, continuing anyway")
		}
	}

	// Load registry cache
	if err := store.loadRegistryCache(); err != nil {
		return nil, err
	}

	// Seed default registries if empty
	if store.registryCacheLen() == 0 {
		if err := store.seedDefaultRegistries(); err != nil {
			return nil, err
		}
	}

	// Start aggregation worker (skip for in-memory databases used in tests)
	if !strings.Contains(cfg.DSN, ":memory:") {
		store.aggregationWorker = NewAggregationWorker(db)
		go store.aggregationWorker.Start()
	} else {
		// For tests: create worker but don't start it
		store.aggregationWorker = NewAggregationWorker(db)
	}

	log.Info().
		Str("driver", cfg.Driver).
		Int("max_open_conns", cfg.MaxOpenConns).
		Int("max_idle_conns", cfg.MaxIdleConns).
		Msg("GORM V2 metadata store initialized")

	return store, nil
}

// loadRegistryCache loads registry name -> ID mapping into memory
func (s *GORMStoreV2) loadRegistryCache() error {
	var registries []RegistryModel
	if err := s.db.Select("id", "name").Find(&registries).Error; err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to load registries")
	}

	s.registryCacheMu.Lock()
	for _, r := range registries {
		s.registryCache[r.Name] = r.ID
	}
	s.registryCacheMu.Unlock()
	return nil
}

// registryCacheLen returns current cache size (used after loading default seed data)
func (s *GORMStoreV2) registryCacheLen() int {
	s.registryCacheMu.RLock()
	defer s.registryCacheMu.RUnlock()
	return len(s.registryCache)
}

// seedDefaultRegistries creates default registry entries
func (s *GORMStoreV2) seedDefaultRegistries() error {
	defaultRegistries := []RegistryModel{
		{Name: "npm", DisplayName: "NPM Registry", UpstreamURL: "https://registry.npmjs.org", Enabled: true, ScanByDefault: true},
		{Name: "pypi", DisplayName: "PyPI", UpstreamURL: "https://pypi.org", Enabled: true, ScanByDefault: true},
		{Name: "go", DisplayName: "Go Modules", UpstreamURL: "https://proxy.golang.org", Enabled: true, ScanByDefault: true},
	}

	for _, reg := range defaultRegistries {
		if err := s.db.Create(&reg).Error; err != nil {
			return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to seed registry: "+reg.Name)
		}
		s.registryCacheMu.Lock()
		s.registryCache[reg.Name] = reg.ID
		s.registryCacheMu.Unlock()
	}

	log.Info().Msg("Seeded default registries: npm, pypi, go")
	return nil
}

// getRegistryID returns the registry ID from cache or database
func (s *GORMStoreV2) getRegistryID(name string) (int32, error) {
	// Fast path: read lock for cache hit
	s.registryCacheMu.RLock()
	if id, ok := s.registryCache[name]; ok {
		s.registryCacheMu.RUnlock()
		return id, nil
	}
	s.registryCacheMu.RUnlock()

	// Not in cache, try to load from database
	var reg RegistryModel
	if err := s.db.Select("id").Where("name = ?", name).First(&reg).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return 0, errors.New(errors.ErrCodeNotFound, "registry not found: "+name)
		}
		return 0, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to query registry")
	}

	// Promote to write lock to populate cache
	s.registryCacheMu.Lock()
	// Re-check: another goroutine may have populated between RUnlock and Lock
	if existing, ok := s.registryCache[name]; ok {
		s.registryCacheMu.Unlock()
		return existing, nil
	}
	s.registryCache[name] = reg.ID
	s.registryCacheMu.Unlock()
	return reg.ID, nil
}

// getStringFromMap safely extracts a string value from a map[string]interface{}
func getStringFromMap(m map[string]interface{}, key string) string {
	if val, ok := m[key]; ok {
		if str, ok := val.(string); ok {
			return str
		}
	}
	return ""
}

// SavePackage saves or updates a package
func (s *GORMStoreV2) SavePackage(ctx context.Context, pkg *metadata.Package) error {
	registryID, err := s.getRegistryID(pkg.Registry)
	if err != nil {
		return err
	}

	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// Convert to model
		model := &PackageModel{
			RegistryID:     registryID,
			Name:           pkg.Name,
			Version:        pkg.Version,
			StorageKey:     pkg.StorageKey,
			Size:           pkg.Size,
			ChecksumMD5:    pkg.ChecksumMD5,
			ChecksumSHA256: pkg.ChecksumSHA256,
			UpstreamURL:    pkg.UpstreamURL,
			CachedAt:       pkg.CachedAt,
			LastAccessed:   pkg.LastAccessed,
			ExpiresAt:      pkg.ExpiresAt,
			RequiresAuth:   pkg.RequiresAuth,
			AuthProvider:   pkg.AuthProvider,
		}

		// Upsert package: first try to update, if no rows affected then create.
		// IMPORTANT: use map[string]interface{} so that zero values
		// (e.g. RequiresAuth=false, Size=0) are written explicitly. With Updates(struct)
		// GORM silently skips zero fields, which leaves stale security-relevant flags.
		updateFields := map[string]interface{}{
			"registry_id":     registryID,
			"name":            pkg.Name,
			"version":         pkg.Version,
			"storage_key":     pkg.StorageKey,
			"size":            pkg.Size,
			"checksum_md5":    pkg.ChecksumMD5,
			"checksum_sha256": pkg.ChecksumSHA256,
			"upstream_url":    pkg.UpstreamURL,
			"cached_at":       pkg.CachedAt,
			"last_accessed":   pkg.LastAccessed,
			"expires_at":      pkg.ExpiresAt,
			"requires_auth":   pkg.RequiresAuth,
			"auth_provider":   pkg.AuthProvider,
		}

		result := tx.Model(&PackageModel{}).
			Where("registry_id = ? AND name = ? AND version = ?", registryID, pkg.Name, pkg.Version).
			Updates(updateFields)

		if result.Error != nil {
			return errors.Wrap(result.Error, errors.ErrCodeStorageFailure, "failed to update package")
		}

		// If no rows were updated, create new record
		if result.RowsAffected == 0 {
			if err := tx.Create(model).Error; err != nil {
				return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to create package")
			}
		} else {
			// On update, fetch the row's primary key for downstream metadata save
			if err := tx.Model(&PackageModel{}).
				Select("id").
				Where("registry_id = ? AND name = ? AND version = ?", registryID, pkg.Name, pkg.Version).
				First(model).Error; err != nil {
				return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to reload package id after update")
			}
		}

		// Save metadata if present
		if len(pkg.Metadata) > 0 {
			// Convert map[string]string to map[string]interface{} for JSONB
			metadataMap := make(map[string]interface{}, len(pkg.Metadata))
			for k, v := range pkg.Metadata {
				metadataMap[k] = v
			}

			metadata := &PackageMetadataModel{
				PackageID:   model.ID,
				RawMetadata: JSONBField(metadataMap),
			}

			// Extract common fields from map[string]string
			if author, ok := pkg.Metadata["author"]; ok {
				metadata.Author = author
			}
			if license, ok := pkg.Metadata["license"]; ok {
				metadata.License = license
			}
			if homepage, ok := pkg.Metadata["homepage"]; ok {
				metadata.Homepage = homepage
			}
			if repo, ok := pkg.Metadata["repository"]; ok {
				metadata.Repository = repo
			}
			if desc, ok := pkg.Metadata["description"]; ok {
				metadata.Description = desc
			}

			if err := tx.Save(metadata).Error; err != nil {
				return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to save metadata")
			}
		}

		return nil
	})
}

// GetPackage retrieves a package by registry, name, and version
func (s *GORMStoreV2) GetPackage(ctx context.Context, registry, name, version string) (*metadata.Package, error) {
	registryID, err := s.getRegistryID(registry)
	if err != nil {
		return nil, err
	}

	var model PackageModel
	result := s.db.WithContext(ctx).
		Preload("Metadata").
		Where("registry_id = ? AND name = ? AND version = ?", registryID, name, version).
		First(&model)

	if result.Error != nil {
		if result.Error == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.ErrCodeNotFound, fmt.Sprintf("package not found: %s/%s@%s", registry, name, version))
		}
		return nil, errors.Wrap(result.Error, errors.ErrCodeStorageFailure, "failed to get package")
	}

	return s.modelToPackage(&model, registry), nil
}

// modelToPackage converts PackageModel to metadata.Package
func (s *GORMStoreV2) modelToPackage(model *PackageModel, registryName string) *metadata.Package {
	pkg := &metadata.Package{
		ID:              fmt.Sprintf("%d", model.ID),
		Registry:        registryName,
		Name:            model.Name,
		Version:         model.Version,
		StorageKey:      model.StorageKey,
		Size:            model.Size,
		ChecksumMD5:     model.ChecksumMD5,
		ChecksumSHA256:  model.ChecksumSHA256,
		UpstreamURL:     model.UpstreamURL,
		CachedAt:        model.CachedAt,
		LastAccessed:    model.LastAccessed,
		ExpiresAt:       model.ExpiresAt,
		DownloadCount:   model.AccessCount,
		SecurityScanned: model.SecurityScanned,
		RequiresAuth:    model.RequiresAuth,
		AuthProvider:    model.AuthProvider,
	}

	// Add metadata if present
	if model.Metadata != nil {
		pkg.Metadata = make(map[string]string)
		for k, v := range model.Metadata.RawMetadata {
			// Convert interface{} values to strings
			if str, ok := v.(string); ok {
				pkg.Metadata[k] = str
			} else {
				// For non-string values, convert to string representation
				pkg.Metadata[k] = fmt.Sprintf("%v", v)
			}
		}
	}

	return pkg
}

// DeletePackage deletes a package (soft delete)
func (s *GORMStoreV2) DeletePackage(ctx context.Context, registry, name, version string) error {
	registryID, err := s.getRegistryID(registry)
	if err != nil {
		return err
	}

	result := s.db.WithContext(ctx).
		Where("registry_id = ? AND name = ? AND version = ?", registryID, name, version).
		Delete(&PackageModel{})

	if result.Error != nil {
		return errors.Wrap(result.Error, errors.ErrCodeStorageFailure, "failed to delete package")
	}

	if result.RowsAffected == 0 {
		return errors.New(errors.ErrCodeNotFound, fmt.Sprintf("package not found: %s/%s@%s", registry, name, version))
	}

	return nil
}

// ListPackages returns packages matching the filter
func (s *GORMStoreV2) ListPackages(ctx context.Context, opts *metadata.ListOptions) ([]*metadata.Package, error) {
	if opts == nil {
		opts = &metadata.ListOptions{}
	}

	query := s.db.WithContext(ctx).Model(&PackageModel{})

	// Apply filters
	if opts.Registry != "" {
		registryID, err := s.getRegistryID(opts.Registry)
		if err != nil {
			return nil, err
		}
		query = query.Where("registry_id = ?", registryID)
	}

	if opts.NamePrefix != "" {
		query = query.Where("name LIKE ?", opts.NamePrefix+"%")
	}

	if opts.ScannedOnly {
		query = query.Where("security_scanned = ?", true)
	}

	if !opts.SinceDate.IsZero() {
		query = query.Where("cached_at >= ?", opts.SinceDate)
	}

	if opts.MinSize > 0 {
		query = query.Where("size >= ?", opts.MinSize)
	}

	if opts.MaxSize > 0 {
		query = query.Where("size <= ?", opts.MaxSize)
	}

	// Apply pagination
	if opts.Limit > 0 {
		query = query.Limit(opts.Limit)
	}
	if opts.Offset > 0 {
		query = query.Offset(opts.Offset)
	}

	// Order by
	if opts.SortBy != "" {
		order := opts.SortBy
		if opts.SortDesc {
			order += " DESC"
		} else {
			order += " ASC"
		}
		query = query.Order(order)
	} else {
		query = query.Order("access_count DESC")
	}

	var models []PackageModel
	if err := query.Find(&models).Error; err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to list packages")
	}

	// Convert to metadata.Package
	packages := make([]*metadata.Package, len(models))
	// Snapshot id->name mapping under read lock once to avoid repeated locking
	s.registryCacheMu.RLock()
	idToName := make(map[int32]string, len(s.registryCache))
	for name, id := range s.registryCache {
		idToName[id] = name
	}
	s.registryCacheMu.RUnlock()
	for i, model := range models {
		packages[i] = s.modelToPackage(&model, idToName[model.RegistryID])
	}

	return packages, nil
}

// UpdateDownloadCount increments download count and records event
func (s *GORMStoreV2) UpdateDownloadCount(ctx context.Context, registry, name, version string) error {
	registryID, err := s.getRegistryID(registry)
	if err != nil {
		return err
	}

	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// Find package
		var pkg PackageModel
		if err := tx.Where("registry_id = ? AND name = ? AND version = ?", registryID, name, version).
			First(&pkg).Error; err != nil {
			return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to find package")
		}

		// Update access count and last accessed
		if err := tx.Model(&pkg).Updates(map[string]interface{}{
			"access_count":  gorm.Expr("access_count + 1"),
			"last_accessed": time.Now(),
		}).Error; err != nil {
			return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to update download count")
		}

		// Record download event
		event := &DownloadEventModel{
			PackageID:    pkg.ID,
			RegistryID:   registryID,
			DownloadedAt: time.Now(),
		}

		if err := tx.Create(event).Error; err != nil {
			return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to record download event")
		}

		return nil
	})
}

// Count returns total number of packages
func (s *GORMStoreV2) Count(ctx context.Context) (int, error) {
	var count int64
	if err := s.db.WithContext(ctx).Model(&PackageModel{}).Count(&count).Error; err != nil {
		return 0, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to count packages")
	}
	return int(count), nil
}

// GetStats returns aggregated statistics for a registry (or all if registry is empty)
func (s *GORMStoreV2) GetStats(ctx context.Context, registry string) (*metadata.Stats, error) {
	stats := &metadata.Stats{
		Registry:    registry,
		LastUpdated: time.Now(),
	}

	query := s.db.WithContext(ctx).Model(&PackageModel{})

	// Filter out metadata entries (npm metadata pages, pypi pages, etc.)
	query = query.Where("version NOT IN (?)", []string{"list", "latest", "metadata", "page"})

	// Filter out Go module metadata files (.mod, .info) - only count actual packages (.zip)
	query = query.Where("name NOT LIKE ?", "%.mod").Where("name NOT LIKE ?", "%.info")

	// Filter by registry if specified
	if registry != "" {
		registryID, err := s.getRegistryID(registry)
		if err != nil {
			return nil, err
		}
		query = query.Where("registry_id = ?", registryID)
	}

	// Total packages
	if err := query.Count(&stats.TotalPackages).Error; err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to count packages")
	}

	// Total size, downloads, scanned, vulnerable, and severity breakdown
	var result struct {
		TotalSize               int64
		TotalDownloads          int64
		ScannedPackages         int64
		VulnerablePackages      int64
		CriticalVulnerabilities int64
		HighVulnerabilities     int64
		ModerateVulnerabilities int64
		LowVulnerabilities      int64
	}

	err := query.
		Select(`
			COALESCE(SUM(size), 0) as total_size,
			COALESCE(SUM(access_count), 0) as total_downloads,
			COALESCE(SUM(CASE WHEN security_scanned THEN 1 ELSE 0 END), 0) as scanned_packages,
			COALESCE(SUM(CASE WHEN vulnerability_count > 0 THEN 1 ELSE 0 END), 0) as vulnerable_packages,
			COALESCE(SUM(CASE WHEN highest_severity = 'critical' THEN 1 ELSE 0 END), 0) as critical_vulnerabilities,
			COALESCE(SUM(CASE WHEN highest_severity = 'high' THEN 1 ELSE 0 END), 0) as high_vulnerabilities,
			COALESCE(SUM(CASE WHEN highest_severity = 'medium' THEN 1 ELSE 0 END), 0) as moderate_vulnerabilities,
			COALESCE(SUM(CASE WHEN highest_severity = 'low' THEN 1 ELSE 0 END), 0) as low_vulnerabilities
		`).
		Scan(&result).Error

	if err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to aggregate stats")
	}

	stats.TotalSize = result.TotalSize
	stats.TotalDownloads = result.TotalDownloads
	stats.ScannedPackages = result.ScannedPackages
	stats.VulnerablePackages = result.VulnerablePackages
	stats.CriticalVulnerabilities = result.CriticalVulnerabilities
	stats.HighVulnerabilities = result.HighVulnerabilities
	stats.ModerateVulnerabilities = result.ModerateVulnerabilities
	stats.LowVulnerabilities = result.LowVulnerabilities

	return stats, nil
}

// Health checks database connectivity
func (s *GORMStoreV2) Health(ctx context.Context) error {
	sqlDB, err := s.db.DB()
	if err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to get sql.DB")
	}

	if err := sqlDB.PingContext(ctx); err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "database ping failed")
	}

	return nil
}

// Close closes the database connection
func (s *GORMStoreV2) Close() error {
	// Stop aggregation worker
	if s.aggregationWorker != nil {
		s.aggregationWorker.Stop()
	}

	sqlDB, err := s.db.DB()
	if err != nil {
		return err
	}
	return sqlDB.Close()
}

// GetTimeSeriesStats returns time-series download statistics
func (s *GORMStoreV2) GetTimeSeriesStats(ctx context.Context, period string, registry string) (*metadata.TimeSeriesStats, error) {
	stats := &metadata.TimeSeriesStats{
		Period:     period,
		Registry:   registry,
		DataPoints: make([]*metadata.TimeSeriesDataPoint, 0),
	}

	// Determine which table to query based on period
	var tableName string
	var since time.Time
	var bucketDuration time.Duration

	switch period {
	case "1h":
		tableName = "download_stats_hourly"
		since = time.Now().Add(-1 * time.Hour).Truncate(time.Hour)
		bucketDuration = time.Hour
	case "1day":
		tableName = "download_stats_hourly"
		since = time.Now().Add(-24 * time.Hour).Truncate(time.Hour)
		bucketDuration = time.Hour
	case "7day":
		tableName = "download_stats_daily"
		since = time.Now().Add(-7 * 24 * time.Hour).Truncate(24 * time.Hour)
		bucketDuration = 24 * time.Hour
	case "30day":
		tableName = "download_stats_daily"
		since = time.Now().Add(-30 * 24 * time.Hour).Truncate(24 * time.Hour)
		bucketDuration = 24 * time.Hour
	default:
		tableName = "download_stats_hourly"
		since = time.Now().Add(-24 * time.Hour).Truncate(time.Hour)
		bucketDuration = time.Hour
	}

	now := time.Now()

	// Try to get registry-level aggregate stats first
	query := s.db.WithContext(ctx).
		Table(tableName).
		Select("time_bucket as timestamp, download_count as value").
		Where("time_bucket >= ? AND time_bucket <= ?", since, now)

	// Filter by registry if specified
	if registry != "" {
		registryID, err := s.getRegistryID(registry)
		if err != nil {
			return nil, err
		}
		query = query.Where("registry_id = ? AND package_id IS NULL", registryID)
	} else {
		query = query.Where("package_id IS NULL")
	}

	query = query.Order("time_bucket ASC")

	type Result struct {
		Timestamp time.Time
		Value     int64
	}

	var results []Result
	if err := query.Scan(&results).Error; err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to get time series stats")
	}

	// If no aggregate data found, try summing package-level stats
	if len(results) == 0 {
		sumQuery := s.db.WithContext(ctx).
			Table(tableName).
			Select("time_bucket as timestamp, SUM(download_count) as value").
			Where("time_bucket >= ? AND time_bucket <= ?", since, now)

		if registry != "" {
			registryID, err := s.getRegistryID(registry)
			if err != nil {
				return nil, err
			}
			sumQuery = sumQuery.Where("registry_id = ? AND package_id IS NOT NULL", registryID)
		} else {
			sumQuery = sumQuery.Where("package_id IS NOT NULL")
		}

		sumQuery = sumQuery.Group("time_bucket").Order("time_bucket ASC")

		if err := sumQuery.Scan(&results).Error; err != nil {
			return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to get time series stats from package data")
		}
	}

	// Create a map of existing data points
	dataMap := make(map[time.Time]int64)
	for _, r := range results {
		dataMap[r.Timestamp] = r.Value
	}

	// Generate continuous time series with 0 values for missing periods
	for t := since; t.Before(now) || t.Equal(now); t = t.Add(bucketDuration) {
		value := int64(0)
		if v, exists := dataMap[t]; exists {
			value = v
		}
		stats.DataPoints = append(stats.DataPoints, &metadata.TimeSeriesDataPoint{
			Timestamp: t,
			Value:     value,
		})
	}

	return stats, nil
}

// AggregateDownloadData aggregates raw download events into hourly/daily stats
func (s *GORMStoreV2) AggregateDownloadData(ctx context.Context) error {
	if s.aggregationWorker == nil {
		return errors.New(errors.ErrCodeStorageFailure, "aggregation worker not initialized")
	}

	// Run hourly aggregation
	if err := s.aggregationWorker.AggregateHourly(); err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to aggregate hourly data")
	}

	// Run daily aggregation
	if err := s.aggregationWorker.AggregateDaily(); err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to aggregate daily data")
	}

	return nil
}

// SaveScanResult saves a security scan result
func (s *GORMStoreV2) SaveScanResult(ctx context.Context, result *metadata.ScanResult) error {
	// Get package by registry, name, version
	registryID, err := s.getRegistryID(result.Registry)
	if err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "registry not found")
	}

	var pkg PackageModel
	if err := s.db.WithContext(ctx).
		Where("registry_id = ? AND name = ? AND version = ?", registryID, result.PackageName, result.PackageVersion).
		First(&pkg).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.New(errors.ErrCodeNotFound, "package not found")
		}
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to find package")
	}

	// Count vulnerabilities by severity
	var criticalCount, highCount, mediumCount, lowCount int
	for _, vuln := range result.Vulnerabilities {
		severity := metadata.NormalizeSeverity(vuln.Severity)
		switch severity {
		case "CRITICAL":
			criticalCount++
		case "HIGH":
			highCount++
		case "MODERATE":
			mediumCount++
		case "LOW":
			lowCount++
		}
	}

	// Prepare Details field - merge scanner details with vulnerabilities
	details := make(map[string]interface{})
	if result.Details != nil {
		for k, v := range result.Details {
			details[k] = v
		}
	}
	// Store vulnerabilities array for later retrieval
	details["vulnerabilities"] = result.Vulnerabilities

	// Create scan result model
	scanModel := &ScanResultModel{
		PackageID:     pkg.ID,
		Scanner:       result.Scanner,
		ScannedAt:     result.ScannedAt,
		Status:        string(result.Status),
		VulnCount:     result.VulnerabilityCount,
		CriticalCount: criticalCount,
		HighCount:     highCount,
		MediumCount:   mediumCount,
		LowCount:      lowCount,
		Details:       JSONBField(details),
	}

	if err := s.db.WithContext(ctx).Create(scanModel).Error; err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to save scan result")
	}

	// Update package security fields
	highestSeverity := "none"
	if criticalCount > 0 {
		highestSeverity = "critical"
	} else if highCount > 0 {
		highestSeverity = "high"
	} else if mediumCount > 0 {
		highestSeverity = "medium"
	} else if lowCount > 0 {
		highestSeverity = "low"
	}

	now := time.Now()
	updates := map[string]interface{}{
		"security_scanned":    true,
		"last_scanned_at":     now,
		"vulnerability_count": result.VulnerabilityCount,
		"highest_severity":    highestSeverity,
		"critical_count":      criticalCount,
		"high_count":          highCount,
		"moderate_count":      mediumCount,
		"low_count":           lowCount,
	}

	if err := s.db.WithContext(ctx).Model(&PackageModel{}).
		Where("id = ?", pkg.ID).
		Updates(updates).Error; err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to update package security fields")
	}

	return nil
}

// GetScanResult retrieves the latest security scan result for a package
func (s *GORMStoreV2) GetScanResult(ctx context.Context, registry, name, version string) (*metadata.ScanResult, error) {
	// Get package by registry, name, version
	registryID, err := s.getRegistryID(registry)
	if err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "registry not found")
	}

	var pkg PackageModel
	if err := s.db.WithContext(ctx).
		Where("registry_id = ? AND name = ? AND version = ?", registryID, name, version).
		First(&pkg).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.ErrCodeNotFound, "package not found")
		}
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to find package")
	}

	// Get latest scan result for this package
	var scanModel ScanResultModel
	if err := s.db.WithContext(ctx).
		Where("package_id = ?", pkg.ID).
		Order("scanned_at DESC").
		First(&scanModel).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, errors.New(errors.ErrCodeNotFound, "scan result not found")
		}
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to retrieve scan result")
	}

	// Extract vulnerabilities from Details
	var vulnerabilities []metadata.Vulnerability
	if vulnData, ok := scanModel.Details["vulnerabilities"]; ok {
		// The vulnerabilities are stored as []interface{} after JSON unmarshaling
		if vulnArray, ok := vulnData.([]interface{}); ok {
			for _, v := range vulnArray {
				if vulnMap, ok := v.(map[string]interface{}); ok {
					vuln := metadata.Vulnerability{
						ID:          getStringFromMap(vulnMap, "id"),
						Severity:    getStringFromMap(vulnMap, "severity"),
						Title:       getStringFromMap(vulnMap, "title"),
						Description: getStringFromMap(vulnMap, "description"),
						FixedIn:     getStringFromMap(vulnMap, "fixed_in"),
					}
					// Extract references array
					if refs, ok := vulnMap["references"].([]interface{}); ok {
						for _, ref := range refs {
							if refStr, ok := ref.(string); ok {
								vuln.References = append(vuln.References, refStr)
							}
						}
					}
					// Extract detected_by array
					if detectedBy, ok := vulnMap["detected_by"].([]interface{}); ok {
						for _, db := range detectedBy {
							if dbStr, ok := db.(string); ok {
								vuln.DetectedBy = append(vuln.DetectedBy, dbStr)
							}
						}
					}
					vulnerabilities = append(vulnerabilities, vuln)
				}
			}
		}
	}

	// Convert to metadata.ScanResult
	result := &metadata.ScanResult{
		ID:                 fmt.Sprintf("%d", scanModel.ID),
		Registry:           registry,
		PackageName:        name,
		PackageVersion:     version,
		Scanner:            scanModel.Scanner,
		Status:             metadata.ScanStatus(scanModel.Status),
		ScannedAt:          scanModel.ScannedAt,
		VulnerabilityCount: scanModel.VulnCount,
		Details:            map[string]interface{}(scanModel.Details),
		Vulnerabilities:    vulnerabilities,
	}

	return result, nil
}

// SaveCVEBypass saves a CVE bypass
func (s *GORMStoreV2) SaveCVEBypass(ctx context.Context, bypass *metadata.CVEBypass) error {
	// Convert metadata.CVEBypass to CVEBypassModel
	model := &CVEBypassModel{
		Type:           string(bypass.Type),
		Target:         bypass.Target,
		Reason:         bypass.Reason,
		CreatedBy:      bypass.CreatedBy,
		ExpiresAt:      bypass.ExpiresAt,
		NotifyOnExpiry: bypass.NotifyOnExpiry,
		Active:         bypass.Active,
	}

	// If ID is provided, try to update existing bypass
	if bypass.ID != "" {
		id, err := strconv.ParseInt(bypass.ID, 10, 64)
		if err == nil {
			model.ID = id
			if err := s.db.WithContext(ctx).Save(model).Error; err != nil {
				return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to update CVE bypass")
			}
			return nil
		}
	}

	// Create new bypass
	if err := s.db.WithContext(ctx).Create(model).Error; err != nil {
		return errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to create CVE bypass")
	}

	// Update the ID in the passed bypass
	bypass.ID = fmt.Sprintf("%d", model.ID)
	bypass.CreatedAt = model.CreatedAt

	return nil
}

// GetActiveCVEBypasses retrieves all active (non-expired) CVE bypasses
func (s *GORMStoreV2) GetActiveCVEBypasses(ctx context.Context) ([]*metadata.CVEBypass, error) {
	var models []CVEBypassModel

	now := time.Now()
	if err := s.db.WithContext(ctx).
		Where("active = ? AND expires_at > ?", true, now).
		Find(&models).Error; err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to retrieve active CVE bypasses")
	}

	// Convert models to metadata.CVEBypass
	bypasses := make([]*metadata.CVEBypass, len(models))
	for i, model := range models {
		bypasses[i] = &metadata.CVEBypass{
			ID:             fmt.Sprintf("%d", model.ID),
			Type:           metadata.BypassType(model.Type),
			Target:         model.Target,
			Reason:         model.Reason,
			CreatedBy:      model.CreatedBy,
			CreatedAt:      model.CreatedAt,
			ExpiresAt:      model.ExpiresAt,
			NotifyOnExpiry: model.NotifyOnExpiry,
			Active:         model.Active,
		}
	}

	return bypasses, nil
}

// ListCVEBypasses lists CVE bypasses with filtering options
func (s *GORMStoreV2) ListCVEBypasses(ctx context.Context, opts *metadata.BypassListOptions) ([]*metadata.CVEBypass, error) {
	query := s.db.WithContext(ctx).Model(&CVEBypassModel{})

	// Apply filters if options provided
	if opts != nil {
		// Filter by type
		if opts.Type != "" {
			query = query.Where("type = ?", string(opts.Type))
		}

		// Filter by active status
		if opts.ActiveOnly {
			query = query.Where("active = ?", true)
		}

		// Filter expired/non-expired
		if !opts.IncludeExpired {
			query = query.Where("expires_at > ?", time.Now())
		}

		// Pagination
		if opts.Limit > 0 {
			query = query.Limit(opts.Limit)
		}
		if opts.Offset > 0 {
			query = query.Offset(opts.Offset)
		}
	}

	// Order by created_at descending (newest first)
	query = query.Order("created_at DESC")

	var models []CVEBypassModel
	if err := query.Find(&models).Error; err != nil {
		return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to list CVE bypasses")
	}

	// Convert models to metadata.CVEBypass
	bypasses := make([]*metadata.CVEBypass, len(models))
	for i, model := range models {
		bypasses[i] = &metadata.CVEBypass{
			ID:             fmt.Sprintf("%d", model.ID),
			Type:           metadata.BypassType(model.Type),
			Target:         model.Target,
			Reason:         model.Reason,
			CreatedBy:      model.CreatedBy,
			CreatedAt:      model.CreatedAt,
			ExpiresAt:      model.ExpiresAt,
			NotifyOnExpiry: model.NotifyOnExpiry,
			Active:         model.Active,
		}
	}

	return bypasses, nil
}

// DeleteCVEBypass deletes a CVE bypass by ID (soft delete)
func (s *GORMStoreV2) DeleteCVEBypass(ctx context.Context, id string) error {
	// Parse ID
	bypassID, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		return errors.New(errors.ErrCodeBadRequest, "invalid bypass ID")
	}

	// Soft delete the bypass
	result := s.db.WithContext(ctx).Delete(&CVEBypassModel{}, bypassID)
	if result.Error != nil {
		return errors.Wrap(result.Error, errors.ErrCodeStorageFailure, "failed to delete CVE bypass")
	}

	if result.RowsAffected == 0 {
		return errors.New(errors.ErrCodeNotFound, "CVE bypass not found")
	}

	return nil
}

// CleanupExpiredBypasses removes expired CVE bypasses
func (s *GORMStoreV2) CleanupExpiredBypasses(ctx context.Context) (int, error) {
	now := time.Now()

	// Hard delete expired bypasses (bypass soft delete with Unscoped)
	result := s.db.WithContext(ctx).
		Unscoped().
		Where("expires_at <= ?", now).
		Delete(&CVEBypassModel{})

	if result.Error != nil {
		return 0, errors.Wrap(result.Error, errors.ErrCodeStorageFailure, "failed to cleanup expired CVE bypasses")
	}

	return int(result.RowsAffected), nil
}
