package recommendations

import (
	"mal/integrations/jikan"
	"math"
	"time"
)

func profileSearchRankWeight(rank int) float64 {
	return math.Max(0.35, 1-(float64(rank)*0.08))
}

func rankedCandidateRetrievalScore(collaborativeScore float64, profileSearchScore float64) float64 {
	return (math.Log1p(collaborativeScore) * collaborativeWeight) +
		(profileSearchScore * profileSearchWeight)
}

func hasTasteMetadata(anime jikan.Anime) bool {
	return len(anime.Genres) > 0 ||
		len(anime.Themes) > 0 ||
		len(anime.Studios) > 0 ||
		len(anime.Demographics) > 0
}

func scoreRecommendationCandidate(
	now time.Time,
	profile userTasteProfile,
	candidate jikan.Anime,
	collaborativeScore float64,
	profileSearchScore float64,
) recommendationCandidate {
	genreMatches, genreScore := weightedEntityMatch(profile.genres, candidate.Genres)
	themeMatches, themeScore := weightedEntityMatch(profile.themes, candidate.Themes)
	studioMatches, studioScore := weightedEntityMatch(profile.studios, candidate.Studios)
	demographicMatches, demographicScore := weightedEntityMatch(profile.demographics, candidate.Demographics)

	score := rankedCandidateRetrievalScore(collaborativeScore, profileSearchScore)
	score += genreScore * genreMatchWeight
	score += themeScore * themeMatchWeight
	score += studioScore * studioMatchWeight
	score += demographicScore * demographicMatchWeight
	score += recommendationCandidateScoreAdjustments(now, profile, candidate)

	return recommendationCandidate{
		anime:              candidate,
		score:              score,
		genreMatches:       genreMatches,
		themeMatches:       themeMatches,
		studioMatches:      studioMatches,
		demographicMatches: demographicMatches,
	}
}

func recommendationCandidateScoreAdjustments(now time.Time, profile userTasteProfile, candidate jikan.Anime) float64 {
	var score float64

	if candidate.Score > 0 {
		score += min(candidate.Score/10.0, 1.0)
	}
	if candidate.Popularity > 0 {
		score += 1.0 / math.Log(float64(candidate.Popularity)+8)
	}
	if profile.prefersAiring && candidate.Airing {
		score += 0.5
	}
	if profile.prefersRecent && isRecentCandidate(now, candidate.Year) {
		score += 0.45
	}
	if isClassicCandidate(now, candidate.Year) {
		score -= 0.2
	}
	if candidate.Status == "Not yet aired" {
		score -= 0.35
	}
	if isFreshRelease(now, candidate.Aired.From) {
		score += 0.3
	}

	return score
}

func isRecentCandidate(now time.Time, year int) bool {
	return year > 0 && now.Year()-year <= 4
}

func isClassicCandidate(now time.Time, year int) bool {
	return year > 0 && now.Year()-year > 15
}

func isFreshRelease(now time.Time, airedFrom string) bool {
	if airedFrom == "" {
		return false
	}

	airedAt, err := time.Parse(time.RFC3339, airedFrom)
	if err != nil {
		return false
	}

	return now.Sub(airedAt) <= freshReleaseWindow
}

func weightedEntityMatch(weights map[int]float64, entities []jikan.NamedEntity) (int, float64) {
	var matches int
	var score float64

	for _, entity := range entities {
		weight, ok := weights[entity.MalID]
		if !ok {
			continue
		}
		matches++
		score += weight
	}

	return matches, score
}
