package services

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

	"github.com/google/uuid"

	"veza-backend-api/internal/models"

	"go.uber.org/zap"
	"gorm.io/gorm"
)

// PlaylistRecommendationService gère les recommandations de playlists
// T0498: Create Playlist Recommendations
type PlaylistRecommendationService struct {
	db                    *gorm.DB
	playlistService       PlaylistServiceForRecommendation
	playlistFollowService PlaylistFollowServiceForRecommendation
	logger                *zap.Logger
}

// PlaylistServiceForRecommendation définit l'interface minimale nécessaire pour les recommandations
// MIGRATION UUID: userID migré vers *uuid.UUID, playlistID en uuid.UUID
type PlaylistServiceForRecommendation interface {
	GetPlaylist(ctx context.Context, playlistID uuid.UUID, userID *uuid.UUID) (*models.Playlist, error)
	GetPlaylists(ctx context.Context, currentUserID *uuid.UUID, filterUserID *uuid.UUID, page, limit int) ([]*models.Playlist, int64, error)
}

// PlaylistFollowServiceForRecommendation définit l'interface minimale nécessaire pour les recommandations
type PlaylistFollowServiceForRecommendation interface {
	GetFollowedPlaylists(ctx context.Context, userID uuid.UUID) ([]*models.Playlist, error)
}

// NewPlaylistRecommendationService crée un nouveau service de recommandations de playlists
func NewPlaylistRecommendationService(
	db *gorm.DB,
	playlistService PlaylistServiceForRecommendation,
	playlistFollowService PlaylistFollowServiceForRecommendation,
	logger *zap.Logger,
) *PlaylistRecommendationService {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &PlaylistRecommendationService{
		db:                    db,
		playlistService:       playlistService,
		playlistFollowService: playlistFollowService,
		logger:                logger,
	}
}

// RecommendationScore représente un score de recommandation pour une playlist
type RecommendationScore struct {
	Playlist *models.Playlist
	Score    float64
	Reason   string
}

// GetRecommendationsParams représente les paramètres pour obtenir des recommandations
// MIGRATION UUID: UserID migré vers uuid.UUID
type GetRecommendationsParams struct {
	UserID     uuid.UUID
	Limit      int     // Nombre de recommandations à retourner (défaut: 20)
	MinScore   float64 // Score minimum pour inclure une recommandation (défaut: 0.1)
	IncludeOwn bool    // Inclure les playlists de l'utilisateur (défaut: false)
}

// GetRecommendations retourne des recommandations de playlists pour un utilisateur
// T0498: Create Playlist Recommendations
func (s *PlaylistRecommendationService) GetRecommendations(
	ctx context.Context,
	params GetRecommendationsParams,
) ([]*RecommendationScore, error) {
	if params.Limit <= 0 {
		params.Limit = 20
	}
	if params.Limit > 100 {
		params.Limit = 100
	}
	if params.MinScore < 0 {
		params.MinScore = 0.1
	}

	// Récupérer les playlists suivies par l'utilisateur
	followedPlaylists, err := s.playlistFollowService.GetFollowedPlaylists(ctx, params.UserID)
	if err != nil {
		s.logger.Warn("Failed to get followed playlists for recommendations",
			zap.String("user_id", params.UserID.String()),
			zap.Error(err))
		followedPlaylists = []*models.Playlist{}
	}

	// Récupérer toutes les playlists publiques (ou accessibles)
	allPlaylists, _, err := s.playlistService.GetPlaylists(ctx, &params.UserID, nil, 1, 1000)
	if err != nil {
		return nil, fmt.Errorf("failed to get playlists: %w", err)
	}

	// Calculer les scores pour chaque playlist
	scores := make([]*RecommendationScore, 0)
	scoreMap := make(map[uuid.UUID]*RecommendationScore)

	for _, playlist := range allPlaylists {
		// Ignorer les playlists de l'utilisateur si IncludeOwn est false
		if !params.IncludeOwn && playlist.UserID == params.UserID {
			continue
		}

		// Ignorer les playlists déjà suivies
		if s.isPlaylistFollowed(playlist.ID, followedPlaylists) {
			continue
		}

		score := s.calculateRecommendationScore(ctx, playlist, params.UserID, followedPlaylists)
		if score.Score >= params.MinScore {
			scoreMap[playlist.ID] = score
		}
	}

	// Convertir la map en slice
	for _, score := range scoreMap {
		scores = append(scores, score)
	}

	// Trier par score décroissant
	sort.Slice(scores, func(i, j int) bool {
		return scores[i].Score > scores[j].Score
	})

	// Limiter le nombre de résultats
	if len(scores) > params.Limit {
		scores = scores[:params.Limit]
	}

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

	return scores, nil
}

// calculateRecommendationScore calcule un score de recommandation pour une playlist
// MIGRATION UUID: userID migré vers uuid.UUID
func (s *PlaylistRecommendationService) calculateRecommendationScore(
	ctx context.Context,
	playlist *models.Playlist,
	_ uuid.UUID,
	followedPlaylists []*models.Playlist,
) *RecommendationScore {
	score := 0.0
	reasons := make([]string, 0)

	// 1. Score basé sur la similarité avec les playlists suivies (poids: 0.5)
	if len(followedPlaylists) > 0 {
		similarityScore := s.calculateSimilarityScore(ctx, playlist, followedPlaylists)
		score += similarityScore * 0.5
		if similarityScore > 0.1 {
			reasons = append(reasons, fmt.Sprintf("Similaire aux playlists suivies (%.2f)", similarityScore))
		}
	}

	// 2. Score basé sur la popularité (nombre de followers) (poids: 0.2)
	popularityScore := s.calculatePopularityScore(playlist)
	score += popularityScore * 0.2
	if popularityScore > 0.1 {
		reasons = append(reasons, fmt.Sprintf("Populaire (%.2f followers)", float64(playlist.FollowerCount)))
	}

	// 3. Score basé sur le nombre de tracks (poids: 0.1)
	trackCountScore := s.calculateTrackCountScore(playlist)
	score += trackCountScore * 0.1
	if trackCountScore > 0.1 {
		reasons = append(reasons, fmt.Sprintf("Contenu riche (%d tracks)", playlist.TrackCount))
	}

	// 4. Score basé sur la récence (poids: 0.2)
	recencyScore := s.calculateRecencyScore(playlist)
	score += recencyScore * 0.2
	if recencyScore > 0.1 {
		reasons = append(reasons, "Récente")
	}

	// Normaliser le score entre 0 et 1
	normalizedScore := math.Min(score, 1.0)

	reason := "Recommandation basée sur plusieurs facteurs"
	if len(reasons) > 0 {
		reason = reasons[0] // Prendre la raison principale
	}

	return &RecommendationScore{
		Playlist: playlist,
		Score:    normalizedScore,
		Reason:   reason,
	}
}

// calculateSimilarityScore calcule un score de similarité basé sur les tracks communs
func (s *PlaylistRecommendationService) calculateSimilarityScore(
	_ context.Context,
	playlist *models.Playlist,
	followedPlaylists []*models.Playlist,
) float64 {
	if len(playlist.Tracks) == 0 {
		return 0.0
	}

	// Récupérer les tracks de la playlist cible
	targetTrackIDs := make(map[uuid.UUID]bool)
	for _, pt := range playlist.Tracks {
		targetTrackIDs[pt.TrackID] = true
	}

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

	// Calculer la similarité avec chaque playlist suivie
	totalSimilarity := 0.0
	validComparisons := 0

	for _, followed := range followedPlaylists {
		if len(followed.Tracks) == 0 {
			continue
		}

		// Récupérer les tracks de la playlist suivie
		followedTrackIDs := make(map[uuid.UUID]bool)
		for _, pt := range followed.Tracks {
			followedTrackIDs[pt.TrackID] = true
		}

		if len(followedTrackIDs) == 0 {
			continue
		}

		// Calculer l'intersection (tracks communs)
		commonTracks := 0
		for trackID := range targetTrackIDs {
			if followedTrackIDs[trackID] {
				commonTracks++
			}
		}

		// Calculer le coefficient de Jaccard (similarité)
		unionSize := len(targetTrackIDs) + len(followedTrackIDs) - commonTracks
		if unionSize > 0 {
			similarity := float64(commonTracks) / float64(unionSize)
			totalSimilarity += similarity
			validComparisons++
		}
	}

	if validComparisons == 0 {
		return 0.0
	}

	// Moyenne des similarités
	return totalSimilarity / float64(validComparisons)
}

// calculatePopularityScore calcule un score basé sur la popularité (nombre de followers)
func (s *PlaylistRecommendationService) calculatePopularityScore(playlist *models.Playlist) float64 {
	// Normaliser le nombre de followers (logarithmique pour éviter que les très grandes valeurs dominent)
	// On considère qu'un playlist avec 100+ followers est très populaire
	maxFollowers := 100.0
	followers := float64(playlist.FollowerCount)

	if followers <= 0 {
		return 0.0
	}

	// Utiliser une fonction logarithmique pour normaliser
	normalized := math.Log10(followers+1) / math.Log10(maxFollowers+1)
	return math.Min(normalized, 1.0)
}

// calculateTrackCountScore calcule un score basé sur le nombre de tracks
func (s *PlaylistRecommendationService) calculateTrackCountScore(playlist *models.Playlist) float64 {
	// On considère qu'une playlist avec 20+ tracks a un bon contenu
	optimalTrackCount := 20.0
	trackCount := float64(playlist.TrackCount)

	if trackCount <= 0 {
		return 0.0
	}

	// Score qui augmente jusqu'à optimalTrackCount, puis se stabilise
	if trackCount >= optimalTrackCount {
		return 1.0
	}

	return trackCount / optimalTrackCount
}

// calculateRecencyScore calcule un score basé sur la récence de la playlist
func (s *PlaylistRecommendationService) calculateRecencyScore(playlist *models.Playlist) float64 {
	if playlist.CreatedAt.IsZero() {
		return 0.0
	}

	// Calculer l'âge en jours
	ageInDays := float64(time.Since(playlist.CreatedAt).Hours() / 24)
	// Si UpdatedAt est plus récent que CreatedAt, utiliser UpdatedAt
	if !playlist.UpdatedAt.IsZero() && playlist.UpdatedAt.After(playlist.CreatedAt) {
		ageInDays = float64(time.Since(playlist.UpdatedAt).Hours() / 24)
	}

	// Les playlists créées/mises à jour dans les 30 derniers jours ont un score élevé
	maxAge := 30.0
	age := ageInDays

	if age <= 0 {
		return 1.0 // Très récente
	}

	if age >= maxAge {
		return 0.0 // Ancienne
	}

	// Score qui diminue linéairement avec l'âge
	return 1.0 - (age / maxAge)
}

// isPlaylistFollowed vérifie si une playlist est dans la liste des playlists suivies
func (s *PlaylistRecommendationService) isPlaylistFollowed(playlistID uuid.UUID, followedPlaylists []*models.Playlist) bool {
	for _, followed := range followedPlaylists {
		if followed.ID == playlistID {
			return true
		}
	}
	return false
}
