// Package gorm provides GORM-based database operations for claude-mnemonic.
package gorm

import (
	"context"
	"database/sql"
	"fmt"
	"sync"
	"time"

	"gorm.io/gorm"

	"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
)

// PatternCleanupFunc is a callback for when patterns are deleted.
type PatternCleanupFunc func(ctx context.Context, deletedIDs []int64)

// PatternStore provides pattern-related database operations using GORM.
type PatternStore struct {
	db          *gorm.DB
	cleanupFunc PatternCleanupFunc
	cleanupMu   sync.RWMutex
}

// NewPatternStore creates a new pattern store.
func NewPatternStore(store *Store) *PatternStore {
	return &PatternStore{
		db: store.DB,
	}
}

// SetCleanupFunc sets the callback for when patterns are deleted.
func (s *PatternStore) SetCleanupFunc(fn PatternCleanupFunc) {
	s.cleanupMu.Lock()
	defer s.cleanupMu.Unlock()
	s.cleanupFunc = fn
}

// StorePattern stores a new pattern.
func (s *PatternStore) StorePattern(ctx context.Context, pattern *models.Pattern) (int64, error) {
	dbPattern := &Pattern{
		Name:            pattern.Name,
		Type:            pattern.Type,
		Signature:       pattern.Signature,
		Frequency:       pattern.Frequency,
		Projects:        pattern.Projects,
		ObservationIDs:  pattern.ObservationIDs,
		Status:          pattern.Status,
		Confidence:      pattern.Confidence,
		LastSeenAt:      pattern.LastSeenAt,
		LastSeenAtEpoch: pattern.LastSeenEpoch,
		CreatedAt:       pattern.CreatedAt,
		CreatedAtEpoch:  pattern.CreatedAtEpoch,
	}

	if pattern.Description.Valid {
		dbPattern.Description = sql.NullString{String: pattern.Description.String, Valid: true}
	}

	if pattern.Recommendation.Valid {
		dbPattern.Recommendation = sql.NullString{String: pattern.Recommendation.String, Valid: true}
	}

	if pattern.MergedIntoID.Valid {
		dbPattern.MergedIntoID = sql.NullInt64{Int64: pattern.MergedIntoID.Int64, Valid: true}
	}

	result := s.db.WithContext(ctx).Create(dbPattern)
	if result.Error != nil {
		return 0, result.Error
	}

	return dbPattern.ID, nil
}

// UpdatePattern updates an existing pattern.
func (s *PatternStore) UpdatePattern(ctx context.Context, pattern *models.Pattern) error {
	updates := map[string]interface{}{
		"name":               pattern.Name,
		"type":               pattern.Type,
		"signature":          pattern.Signature,
		"frequency":          pattern.Frequency,
		"projects":           pattern.Projects,
		"observation_ids":    pattern.ObservationIDs,
		"status":             pattern.Status,
		"confidence":         pattern.Confidence,
		"last_seen_at":       pattern.LastSeenAt,
		"last_seen_at_epoch": pattern.LastSeenEpoch,
	}

	if pattern.Description.Valid {
		updates["description"] = pattern.Description.String
	} else {
		updates["description"] = nil
	}

	if pattern.Recommendation.Valid {
		updates["recommendation"] = pattern.Recommendation.String
	} else {
		updates["recommendation"] = nil
	}

	if pattern.MergedIntoID.Valid {
		updates["merged_into_id"] = pattern.MergedIntoID.Int64
	} else {
		updates["merged_into_id"] = nil
	}

	result := s.db.WithContext(ctx).
		Model(&Pattern{}).
		Where("id = ?", pattern.ID).
		Updates(updates)

	return result.Error
}

// GetPatternByID retrieves a pattern by ID.
func (s *PatternStore) GetPatternByID(ctx context.Context, id int64) (*models.Pattern, error) {
	var dbPattern Pattern

	err := s.db.WithContext(ctx).First(&dbPattern, id).Error
	if err == gorm.ErrRecordNotFound {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	return toModelPattern(&dbPattern), nil
}

// GetPatternByName retrieves a pattern by name.
func (s *PatternStore) GetPatternByName(ctx context.Context, name string) (*models.Pattern, error) {
	var dbPattern Pattern

	err := s.db.WithContext(ctx).
		Where("name = ? AND status = ?", name, models.PatternStatusActive).
		First(&dbPattern).Error

	if err == gorm.ErrRecordNotFound {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	return toModelPattern(&dbPattern), nil
}

// GetActivePatterns retrieves all active patterns.
func (s *PatternStore) GetActivePatterns(ctx context.Context, limit int) ([]*models.Pattern, error) {
	var patterns []Pattern

	err := s.db.WithContext(ctx).
		Where("status = ?", models.PatternStatusActive).
		Order("frequency DESC, confidence DESC").
		Limit(limit).
		Find(&patterns).Error

	if err != nil {
		return nil, err
	}

	return toModelPatterns(patterns), nil
}

// GetPatternsByType retrieves patterns of a specific type.
func (s *PatternStore) GetPatternsByType(ctx context.Context, patternType models.PatternType, limit int) ([]*models.Pattern, error) {
	var patterns []Pattern

	err := s.db.WithContext(ctx).
		Where("type = ? AND status = ?", patternType, models.PatternStatusActive).
		Order("frequency DESC, confidence DESC").
		Limit(limit).
		Find(&patterns).Error

	if err != nil {
		return nil, err
	}

	return toModelPatterns(patterns), nil
}

// GetPatternsByProject retrieves patterns that have been observed in a specific project.
// Uses raw SQL since JSON_EACH is complex in GORM.
func (s *PatternStore) GetPatternsByProject(ctx context.Context, project string, limit int) ([]*models.Pattern, error) {
	var patterns []Pattern

	// Use raw SQL for JSON_EACH query
	query := `
		SELECT * FROM patterns
		WHERE status = 'active'
		AND EXISTS (
			SELECT 1 FROM json_each(projects)
			WHERE json_each.value = ?
		)
		ORDER BY frequency DESC, confidence DESC
		LIMIT ?
	`

	err := s.db.WithContext(ctx).
		Raw(query, project, limit).
		Scan(&patterns).Error

	if err != nil {
		return nil, err
	}

	return toModelPatterns(patterns), nil
}

// FindMatchingPatterns searches for patterns that match a given signature.
// Pattern matching is done in Go code for simplicity.
func (s *PatternStore) FindMatchingPatterns(ctx context.Context, signature []string, minScore float64) ([]*models.Pattern, error) {
	// Get all active patterns
	patterns, err := s.GetActivePatterns(ctx, 100)
	if err != nil {
		return nil, err
	}

	// Filter by signature match in Go
	var matches []*models.Pattern
	for _, pattern := range patterns {
		score := models.CalculateMatchScore(signature, pattern.Signature)
		if score >= minScore {
			matches = append(matches, pattern)
		}
	}

	return matches, nil
}

// MarkPatternDeprecated marks a pattern as deprecated.
func (s *PatternStore) MarkPatternDeprecated(ctx context.Context, id int64) error {
	result := s.db.WithContext(ctx).
		Model(&Pattern{}).
		Where("id = ?", id).
		Update("status", models.PatternStatusDeprecated)

	return result.Error
}

// MergePatterns merges a source pattern into a target pattern.
func (s *PatternStore) MergePatterns(ctx context.Context, sourceID, targetID int64) error {
	if sourceID == targetID {
		return fmt.Errorf("cannot merge pattern into itself")
	}
	// Get both patterns
	source, err := s.GetPatternByID(ctx, sourceID)
	if err != nil {
		return err
	}
	target, err := s.GetPatternByID(ctx, targetID)
	if err != nil {
		return err
	}

	// Merge source into target
	target.Frequency += source.Frequency

	// Merge projects (deduplicate)
	for _, proj := range source.Projects {
		found := false
		for _, existing := range target.Projects {
			if existing == proj {
				found = true
				break
			}
		}
		if !found {
			target.Projects = append(target.Projects, proj)
		}
	}

	// Merge observation IDs (deduplicate)
	for _, obsID := range source.ObservationIDs {
		found := false
		for _, existing := range target.ObservationIDs {
			if existing == obsID {
				found = true
				break
			}
		}
		if !found {
			target.ObservationIDs = append(target.ObservationIDs, obsID)
		}
	}

	// Update target
	if err := s.UpdatePattern(ctx, target); err != nil {
		return err
	}

	// Mark source as merged
	source.Status = models.PatternStatusMerged
	source.MergedIntoID = sql.NullInt64{Int64: targetID, Valid: true}
	return s.UpdatePattern(ctx, source)
}

// DeletePattern deletes a pattern by ID.
func (s *PatternStore) DeletePattern(ctx context.Context, id int64) error {
	result := s.db.WithContext(ctx).Delete(&Pattern{}, id)

	if result.Error == nil {
		s.cleanupMu.RLock()
		fn := s.cleanupFunc
		s.cleanupMu.RUnlock()
		if fn != nil {
			fn(ctx, []int64{id})
		}
	}

	return result.Error
}

// SearchPatternsFTS performs full-text search on patterns.
// Uses raw SQL for FTS5 query.
func (s *PatternStore) SearchPatternsFTS(ctx context.Context, searchQuery string, limit int) ([]*models.Pattern, error) {
	var patterns []Pattern

	// Use raw SQL for FTS5 MATCH query
	query := `
		SELECT p.*
		FROM patterns p
		JOIN patterns_fts fts ON p.id = fts.rowid
		WHERE patterns_fts MATCH ?
		AND p.status = 'active'
		ORDER BY rank
		LIMIT ?
	`

	err := s.db.WithContext(ctx).
		Raw(query, searchQuery, limit).
		Scan(&patterns).Error

	if err != nil {
		return nil, err
	}

	return toModelPatterns(patterns), nil
}

// PatternStats contains aggregate statistics about patterns.
type PatternStats struct {
	Total            int     `json:"total"`
	Active           int     `json:"active"`
	Deprecated       int     `json:"deprecated"`
	Merged           int     `json:"merged"`
	TotalOccurrences int     `json:"total_occurrences"`
	AvgConfidence    float64 `json:"avg_confidence"`
	Bugs             int     `json:"bugs"`
	Refactors        int     `json:"refactors"`
	Architectures    int     `json:"architectures"`
	AntiPatterns     int     `json:"anti_patterns"`
	BestPractices    int     `json:"best_practices"`
}

// GetPatternStats returns statistics about patterns.
// Uses raw SQL for complex aggregate query.
func (s *PatternStore) GetPatternStats(ctx context.Context) (*PatternStats, error) {
	var stats PatternStats

	query := `
		SELECT
			COUNT(*) as total,
			COUNT(CASE WHEN status = 'active' THEN 1 END) as active,
			COUNT(CASE WHEN status = 'deprecated' THEN 1 END) as deprecated,
			COUNT(CASE WHEN status = 'merged' THEN 1 END) as merged,
			COALESCE(SUM(frequency), 0) as total_occurrences,
			COALESCE(AVG(confidence), 0) as avg_confidence,
			COUNT(CASE WHEN type = 'bug' THEN 1 END) as bugs,
			COUNT(CASE WHEN type = 'refactor' THEN 1 END) as refactors,
			COUNT(CASE WHEN type = 'architecture' THEN 1 END) as architectures,
			COUNT(CASE WHEN type = 'anti-pattern' THEN 1 END) as anti_patterns,
			COUNT(CASE WHEN type = 'best-practice' THEN 1 END) as best_practices
		FROM patterns
	`

	err := s.db.WithContext(ctx).Raw(query).Scan(&stats).Error
	return &stats, err
}

// IncrementPatternFrequency atomically increments a pattern's frequency and updates last_seen.
func (s *PatternStore) IncrementPatternFrequency(ctx context.Context, id int64, project string, observationID int64) error {
	now := time.Now()

	// Get current pattern
	pattern, err := s.GetPatternByID(ctx, id)
	if err != nil {
		return err
	}

	// Add occurrence
	pattern.AddOccurrence(project, observationID)
	pattern.LastSeenAt = now.Format(time.RFC3339)
	pattern.LastSeenEpoch = now.UnixMilli()

	return s.UpdatePattern(ctx, pattern)
}

// toModelPattern converts a GORM Pattern to a pkg/models Pattern.
func toModelPattern(p *Pattern) *models.Pattern {
	pattern := &models.Pattern{
		ID:             p.ID,
		Name:           p.Name,
		Type:           p.Type,
		Description:    p.Description,
		Signature:      p.Signature,
		Recommendation: p.Recommendation,
		Frequency:      p.Frequency,
		Projects:       p.Projects,
		ObservationIDs: p.ObservationIDs,
		Status:         p.Status,
		MergedIntoID:   p.MergedIntoID,
		Confidence:     p.Confidence,
		LastSeenAt:     p.LastSeenAt,
		LastSeenEpoch:  p.LastSeenAtEpoch,
		CreatedAt:      p.CreatedAt,
		CreatedAtEpoch: p.CreatedAtEpoch,
	}

	return pattern
}

// toModelPatterns converts a slice of GORM Patterns to pkg/models Patterns.
func toModelPatterns(patterns []Pattern) []*models.Pattern {
	result := make([]*models.Pattern, len(patterns))
	for i, p := range patterns {
		result[i] = toModelPattern(&p)
	}
	return result
}
