package services

import (
	"context"
	"fmt"
	"math"
	"sort"
	"time"

	"github.com/google/uuid"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"veza-backend-api/internal/models"
)

// TrackRecommendationService provides ML-based recommendations for tracks
// BE-SVC-007: Implement recommendation engine
type TrackRecommendationService struct {
	db     *gorm.DB
	logger *zap.Logger
}

// NewTrackRecommendationService creates a new track recommendation service
func NewTrackRecommendationService(db *gorm.DB, logger *zap.Logger) *TrackRecommendationService {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &TrackRecommendationService{
		db:     db,
		logger: logger,
	}
}

// TrackRecommendationParams represents parameters for track recommendations
type TrackRecommendationParams struct {
	UserID      uuid.UUID   // User to get recommendations for
	Limit       int         // Number of recommendations (default: 20, max: 100)
	MinScore    float64     // Minimum recommendation score (default: 0.1)
	SeedTrackID *uuid.UUID  // Optional seed track for content-based recommendations
	Genres      []string    // Optional genre filter
	ExcludeIDs  []uuid.UUID // Track IDs to exclude from results
}

// TrackRecommendation represents a recommended track with score and reason
type TrackRecommendation struct {
	Track  *models.Track `json:"track"`
	Score  float64       `json:"score"`
	Reason string        `json:"reason"`
}

// GetRecommendations returns personalized track recommendations for a user
func (s *TrackRecommendationService) GetRecommendations(
	ctx context.Context,
	params TrackRecommendationParams,
) ([]*TrackRecommendation, error) {
	// Validate and set defaults
	if params.Limit <= 0 {
		params.Limit = 20
	}
	if params.Limit > 100 {
		params.Limit = 100
	}
	if params.MinScore <= 0 {
		params.MinScore = 0.1
	}

	// Get user's listening history
	userHistory, err := s.getUserListeningHistory(ctx, params.UserID)
	if err != nil {
		s.logger.Warn("Failed to get user listening history",
			zap.String("user_id", params.UserID.String()),
			zap.Error(err))
		userHistory = []uuid.UUID{}
	}

	// Get user's liked tracks
	likedTracks, err := s.getUserLikedTracks(ctx, params.UserID)
	if err != nil {
		s.logger.Warn("Failed to get user liked tracks",
			zap.String("user_id", params.UserID.String()),
			zap.Error(err))
		likedTracks = []uuid.UUID{}
	}

	// Combine history and likes for user preferences
	userPreferences := make(map[uuid.UUID]bool)
	for _, trackID := range userHistory {
		userPreferences[trackID] = true
	}
	for _, trackID := range likedTracks {
		userPreferences[trackID] = true
	}

	// Get all public tracks (excluding user's own tracks and excluded IDs)
	var tracks []models.Track
	query := s.db.WithContext(ctx).
		Model(&models.Track{}).
		Where("is_public = ? AND deleted_at IS NULL", true).
		Where("creator_id != ?", params.UserID)

	// Exclude specified track IDs
	if len(params.ExcludeIDs) > 0 {
		query = query.Where("id NOT IN ?", params.ExcludeIDs)
	}

	// Apply genre filter if provided
	if len(params.Genres) > 0 {
		query = query.Where("LOWER(genre) IN ?", params.Genres)
	}

	if err := query.Find(&tracks).Error; err != nil {
		return nil, fmt.Errorf("failed to get tracks: %w", err)
	}

	// Calculate recommendation scores for each track
	recommendations := make([]*TrackRecommendation, 0, len(tracks))
	recommendationMap := make(map[uuid.UUID]*TrackRecommendation)

	for i := range tracks {
		track := &tracks[i]

		// Skip tracks already in user preferences
		if userPreferences[track.ID] {
			continue
		}

		// Calculate recommendation score using multiple algorithms
		score, reason := s.calculateRecommendationScore(
			ctx,
			track,
			params.UserID,
			userHistory,
			likedTracks,
			params.SeedTrackID,
		)

		if score >= params.MinScore {
			recommendationMap[track.ID] = &TrackRecommendation{
				Track:  track,
				Score:  score,
				Reason: reason,
			}
		}
	}

	// Convert map to slice
	for _, rec := range recommendationMap {
		recommendations = append(recommendations, rec)
	}

	// Sort by score (descending)
	sort.Slice(recommendations, func(i, j int) bool {
		return recommendations[i].Score > recommendations[j].Score
	})

	// Limit results
	if len(recommendations) > params.Limit {
		recommendations = recommendations[:params.Limit]
	}

	s.logger.Info("Track recommendations generated",
		zap.String("user_id", params.UserID.String()),
		zap.Int("count", len(recommendations)),
		zap.Int("limit", params.Limit))

	return recommendations, nil
}

// calculateRecommendationScore calculates a recommendation score using multiple algorithms
func (s *TrackRecommendationService) calculateRecommendationScore(
	ctx context.Context,
	track *models.Track,
	userID uuid.UUID,
	userHistory []uuid.UUID,
	likedTracks []uuid.UUID,
	seedTrackID *uuid.UUID,
) (float64, string) {
	totalScore := 0.0
	reasons := []string{}

	// 1. Collaborative Filtering (40% weight)
	// Find users with similar taste and recommend tracks they liked
	collabScore := s.calculateCollaborativeScore(ctx, track, userID, likedTracks)
	totalScore += collabScore * 0.4
	if collabScore > 0.1 {
		reasons = append(reasons, fmt.Sprintf("Similar users like this (%.2f)", collabScore))
	}

	// 2. Content-Based Filtering (30% weight)
	// Recommend tracks similar to user's liked tracks
	contentScore := s.calculateContentBasedScore(ctx, track, userHistory, likedTracks, seedTrackID)
	totalScore += contentScore * 0.3
	if contentScore > 0.1 {
		reasons = append(reasons, fmt.Sprintf("Similar to your preferences (%.2f)", contentScore))
	}

	// 3. Popularity Score (20% weight)
	// Recommend popular tracks
	popularityScore := s.calculatePopularityScore(track)
	totalScore += popularityScore * 0.2
	if popularityScore > 0.1 {
		reasons = append(reasons, fmt.Sprintf("Popular track (%.2f)", popularityScore))
	}

	// 4. Recency Score (10% weight)
	// Recommend recent tracks
	recencyScore := s.calculateRecencyScore(track)
	totalScore += recencyScore * 0.1
	if recencyScore > 0.1 {
		reasons = append(reasons, "Recently uploaded")
	}

	// Normalize score to 0-1 range
	normalizedScore := math.Min(totalScore, 1.0)

	reason := "Recommended based on multiple factors"
	if len(reasons) > 0 {
		reason = reasons[0] // Use primary reason
	}

	return normalizedScore, reason
}

// calculateCollaborativeScore uses collaborative filtering to find tracks liked by similar users
func (s *TrackRecommendationService) calculateCollaborativeScore(
	ctx context.Context,
	track *models.Track,
	userID uuid.UUID,
	userLikedTracks []uuid.UUID,
) float64 {
	if len(userLikedTracks) == 0 {
		return 0.0
	}

	// Find users who liked this track
	var usersWhoLiked []uuid.UUID
	if err := s.db.WithContext(ctx).
		Model(&models.TrackLike{}).
		Where("track_id = ?", track.ID).
		Pluck("user_id", &usersWhoLiked).Error; err != nil {
		return 0.0
	}

	if len(usersWhoLiked) == 0 {
		return 0.0
	}

	// Calculate similarity: how many tracks do these users have in common with current user?
	// Use Jaccard similarity
	commonLikes := 0
	for _, likedTrackID := range userLikedTracks {
		// Check if any of the users who liked this track also liked the user's liked tracks
		var count int64
		s.db.WithContext(ctx).
			Model(&models.TrackLike{}).
			Where("track_id = ? AND user_id IN ?", likedTrackID, usersWhoLiked).
			Count(&count)
		if count > 0 {
			commonLikes++
		}
	}

	if len(userLikedTracks) == 0 {
		return 0.0
	}

	// Jaccard similarity: intersection / union
	similarity := float64(commonLikes) / float64(len(userLikedTracks))

	// Weight by number of users who liked this track (more users = more confidence)
	userCountWeight := math.Log10(float64(len(usersWhoLiked))+1) / math.Log10(100.0+1)
	userCountWeight = math.Min(userCountWeight, 1.0)

	return similarity * userCountWeight
}

// calculateContentBasedScore recommends tracks similar to user's preferences based on metadata
func (s *TrackRecommendationService) calculateContentBasedScore(
	ctx context.Context,
	track *models.Track,
	userHistory []uuid.UUID,
	likedTracks []uuid.UUID,
	seedTrackID *uuid.UUID,
) float64 {
	// Get user's preferred tracks (history + likes)
	preferredTrackIDs := make(map[uuid.UUID]bool)
	for _, id := range userHistory {
		preferredTrackIDs[id] = true
	}
	for _, id := range likedTracks {
		preferredTrackIDs[id] = true
	}

	// If seed track is provided, use it
	if seedTrackID != nil {
		preferredTrackIDs[*seedTrackID] = true
	}

	if len(preferredTrackIDs) == 0 {
		return 0.0
	}

	// Get preferred tracks metadata
	var preferredTracks []models.Track
	if err := s.db.WithContext(ctx).
		Model(&models.Track{}).
		Where("id IN ?", getKeys(preferredTrackIDs)).
		Find(&preferredTracks).Error; err != nil {
		return 0.0
	}

	// Calculate similarity based on genre, artist, and other metadata
	totalSimilarity := 0.0
	validComparisons := 0

	for _, preferred := range preferredTracks {
		similarity := 0.0

		// Genre match (weight: 0.4)
		if track.Genre != "" && preferred.Genre != "" {
			if track.Genre == preferred.Genre {
				similarity += 0.4
			}
		}

		// Artist match (weight: 0.4)
		if track.Artist != "" && preferred.Artist != "" {
			if track.Artist == preferred.Artist {
				similarity += 0.4
			}
		}

		// Year similarity (weight: 0.1)
		if track.Year > 0 && preferred.Year > 0 {
			yearDiff := math.Abs(float64(track.Year - preferred.Year))
			if yearDiff <= 5 {
				similarity += 0.1 * (1.0 - yearDiff/5.0)
			}
		}

		// Format match (weight: 0.1)
		if track.Format != "" && preferred.Format != "" {
			if track.Format == preferred.Format {
				similarity += 0.1
			}
		}

		totalSimilarity += similarity
		validComparisons++
	}

	if validComparisons == 0 {
		return 0.0
	}

	return totalSimilarity / float64(validComparisons)
}

// calculatePopularityScore calculates score based on track popularity
func (s *TrackRecommendationService) calculatePopularityScore(track *models.Track) float64 {
	// Combine play_count and like_count for popularity
	playWeight := 0.6
	likeWeight := 0.4

	// Normalize play_count (logarithmic scale, max 10000 plays = 1.0)
	maxPlays := 10000.0
	playScore := 0.0
	if track.PlayCount > 0 {
		playScore = math.Log10(float64(track.PlayCount)+1) / math.Log10(maxPlays+1)
		playScore = math.Min(playScore, 1.0)
	}

	// Normalize like_count (logarithmic scale, max 1000 likes = 1.0)
	maxLikes := 1000.0
	likeScore := 0.0
	if track.LikeCount > 0 {
		likeScore = math.Log10(float64(track.LikeCount)+1) / math.Log10(maxLikes+1)
		likeScore = math.Min(likeScore, 1.0)
	}

	return (playScore * playWeight) + (likeScore * likeWeight)
}

// calculateRecencyScore calculates score based on track recency
func (s *TrackRecommendationService) calculateRecencyScore(track *models.Track) float64 {
	if track.CreatedAt.IsZero() {
		return 0.0
	}

	// Calculate age in days
	ageInDays := float64(time.Since(track.CreatedAt).Hours() / 24)

	// Tracks created in last 30 days get high score
	maxAge := 30.0
	if ageInDays <= 0 {
		return 1.0 // Very recent
	}
	if ageInDays >= maxAge {
		return 0.0 // Old
	}

	// Score decreases linearly with age
	return 1.0 - (ageInDays / maxAge)
}

// getUserListeningHistory gets tracks the user has played
func (s *TrackRecommendationService) getUserListeningHistory(
	ctx context.Context,
	userID uuid.UUID,
) ([]uuid.UUID, error) {
	var trackIDs []uuid.UUID

	// Get distinct track IDs from track_plays
	if err := s.db.WithContext(ctx).
		Model(&models.TrackPlay{}).
		Where("user_id = ? AND deleted_at IS NULL", userID).
		Distinct("track_id").
		Pluck("track_id", &trackIDs).Error; err != nil {
		return nil, err
	}

	return trackIDs, nil
}

// getUserLikedTracks gets tracks the user has liked
func (s *TrackRecommendationService) getUserLikedTracks(
	ctx context.Context,
	userID uuid.UUID,
) ([]uuid.UUID, error) {
	var trackIDs []uuid.UUID

	if err := s.db.WithContext(ctx).
		Model(&models.TrackLike{}).
		Where("user_id = ?", userID).
		Pluck("track_id", &trackIDs).Error; err != nil {
		return nil, err
	}

	return trackIDs, nil
}

// getKeys extracts keys from a map
func getKeys(m map[uuid.UUID]bool) []uuid.UUID {
	keys := make([]uuid.UUID, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}
