package services

import (
	"encoding/json"
	"math"
	"time"

	"github.com/google/uuid"
	"github.com/tapper/backend/database"
	"github.com/tapper/backend/models"
	"gorm.io/gorm"
)

type EloResult struct {
	UserID   uuid.UUID
	OldMMR   int
	NewMMR   int
	Delta    int
	Metadata map[string]interface{}
}

func GetEloConfig() (*models.EloConfig, error) {
	var cfg models.EloConfig
	if err := database.DB.First(&cfg, 1).Error; err != nil {
		return nil, err
	}
	return &cfg, nil
}

func CalculateElo(match *models.Match, players []models.MatchPlayer, stats []models.MatchPlayerStats) ([]EloResult, error) {
	cfg, err := GetEloConfig()
	if err != nil {
		return nil, err
	}

	userIDs := make([]uuid.UUID, len(players))
	for i, p := range players {
		userIDs[i] = p.UserID
	}
	var users []models.User
	if err := database.DB.Where("id IN ?", userIDs).Find(&users).Error; err != nil {
		return nil, err
	}
	userMap := make(map[uuid.UUID]*models.User)
	for i := range users {
		userMap[users[i].ID] = &users[i]
	}

	statsMap := make(map[uuid.UUID]*models.MatchPlayerStats)
	for i := range stats {
		statsMap[stats[i].UserID] = &stats[i]
	}

	var team1MMRSum, team2MMRSum float64
	var team1Count, team2Count int
	for _, p := range players {
		u := userMap[p.UserID]
		if u == nil {
			continue
		}
		if p.Team == 1 {
			team1MMRSum += float64(u.MMR)
			team1Count++
		} else {
			team2MMRSum += float64(u.MMR)
			team2Count++
		}
	}
	team1AvgMMR := team1MMRSum / math.Max(float64(team1Count), 1)
	team2AvgMMR := team2MMRSum / math.Max(float64(team2Count), 1)

	matchAvg := calcMatchAverageStats(stats)

	var team1Won, isDraw bool
	if match.Team1Score > match.Team2Score {
		team1Won = true
	} else if match.Team1Score == match.Team2Score {
		isDraw = true
	}

	totalRounds := float64(match.Team1Score + match.Team2Score)
	if totalRounds == 0 {
		totalRounds = 1
	}

	results := make([]EloResult, 0, len(players))

	for _, p := range players {
		user := userMap[p.UserID]
		if user == nil {
			continue
		}

		playerStats := statsMap[p.UserID]

		var actualScore float64
		var opponentAvgMMR float64
		var teamRounds, opponentRounds float64

		if p.Team == 1 {
			opponentAvgMMR = team2AvgMMR
			teamRounds = float64(match.Team1Score)
			opponentRounds = float64(match.Team2Score)
			if isDraw {
				actualScore = 0.5
			} else if team1Won {
				actualScore = 1.0
			} else {
				actualScore = 0.0
			}
		} else {
			opponentAvgMMR = team1AvgMMR
			teamRounds = float64(match.Team2Score)
			opponentRounds = float64(match.Team1Score)
			if isDraw {
				actualScore = 0.5
			} else if !team1Won && !isDraw {
				actualScore = 1.0
			} else {
				actualScore = 0.0
			}
		}

		won := actualScore == 1.0

		expected := 1.0 / (1.0 + math.Pow(10, (opponentAvgMMR-float64(user.MMR))/400.0))

		kFactor := selectKFactor(cfg, user)

		baseDelta := float64(kFactor) * (actualScore - expected)

		maxRounds := math.Max(totalRounds, 1)
		roundMargin := (teamRounds - opponentRounds) / maxRounds
		scoreModifier := 1.0 + (roundMargin * cfg.RoundScoreWeight)

		perfModifier := 1.0
		if playerStats != nil && matchAvg.kills > 0 {
			perfScore := calcPerfScore(playerStats, matchAvg)
			perfModifier = 1.0 + ((perfScore - 1.0) * cfg.IndividualPerfWeight)
		}

		streakModifier := 1.0
		if won && user.CurrentStreak > 0 {
			streakCount := math.Min(float64(user.CurrentStreak), float64(cfg.StreakCap))
			streakModifier = 1.0 + (streakCount * cfg.StreakBonus)
		}

		finalDelta := int(math.Round(baseDelta * scoreModifier * perfModifier * streakModifier))
		newMMR := user.MMR + finalDelta
		if newMMR < cfg.MinMMR {
			newMMR = cfg.MinMMR
		}
		if newMMR > cfg.MaxMMR {
			newMMR = cfg.MaxMMR
		}
		finalDelta = newMMR - user.MMR

		metadata := map[string]interface{}{
			"k_factor":        kFactor,
			"expected":        math.Round(expected*1000) / 1000,
			"base_delta":      math.Round(baseDelta*100) / 100,
			"score_modifier":  math.Round(scoreModifier*1000) / 1000,
			"perf_modifier":   math.Round(perfModifier*1000) / 1000,
			"streak_modifier": math.Round(streakModifier*1000) / 1000,
		}

		results = append(results, EloResult{
			UserID:   user.ID,
			OldMMR:   user.MMR,
			NewMMR:   newMMR,
			Delta:    finalDelta,
			Metadata: metadata,
		})
	}

	return results, nil
}

func ApplyEloResults(matchID uuid.UUID, results []EloResult) error {
	tx := database.DB.Begin()
	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
		}
	}()

	for _, r := range results {
		metaJSON, _ := json.Marshal(r.Metadata)
		metaStr := string(metaJSON)

		reason := models.MMRReasonMatch
		if r.Metadata != nil {
			if _, ok := r.Metadata["placement"]; ok {
				reason = models.MMRReasonPlacement
			}
		}

		history := models.MMRHistory{
			UserID:   r.UserID,
			MatchID:  &matchID,
			OldMMR:   r.OldMMR,
			NewMMR:   r.NewMMR,
			Delta:    r.Delta,
			Reason:   reason,
			MetaJSON: &metaStr,
		}
		if err := tx.Create(&history).Error; err != nil {
			tx.Rollback()
			return err
		}

		var result string
		if r.Delta > 0 {
			result = "win"
		} else if r.Delta < 0 {
			result = "loss"
		} else {
			result = "draw"
		}

		updates := map[string]interface{}{
			"mmr":            r.NewMMR,
			"matches_played": gorm.Expr("matches_played + 1"),
			"last_match_at":  time.Now().UTC(),
		}
		if result == "win" {
			updates["wins"] = gorm.Expr("wins + 1")
			updates["current_streak"] = gorm.Expr("current_streak + 1")
		} else if result == "loss" {
			updates["losses"] = gorm.Expr("losses + 1")
			updates["current_streak"] = 0
		}

		if err := tx.Model(&models.User{}).Where("id = ?", r.UserID).Updates(updates).Error; err != nil {
			tx.Rollback()
			return err
		}

		delta := r.Delta
		if err := tx.Model(&models.MatchPlayer{}).
			Where("match_id = ? AND user_id = ?", matchID, r.UserID).
			Updates(map[string]interface{}{
				"result":    result,
				"mmr_delta": &delta,
			}).Error; err != nil {
			tx.Rollback()
			return err
		}

		var user models.User
		if err := tx.First(&user, r.UserID).Error; err == nil {
			var eloCfg models.EloConfig
			if err := tx.First(&eloCfg, 1).Error; err == nil {
				if !user.PlacementDone && user.MatchesPlayed >= eloCfg.PlacementMatches {
					tx.Model(&user).Update("placement_done", true)
				}
			}
		}
	}

	return tx.Commit().Error
}

func selectKFactor(cfg *models.EloConfig, user *models.User) int {
	if user.MatchesPlayed < cfg.PlacementMatches {
		return cfg.PlacementKFactor
	}
	if user.MatchesPlayed < cfg.NewPlayerThreshold {
		return cfg.KFactorNewPlayer
	}
	if user.MMR >= cfg.HighMMRThreshold {
		return cfg.KFactorHighMMR
	}
	return cfg.KFactorBase
}

type avgStats struct {
	kills     float64
	deaths    float64
	damage    float64
	kd        float64
	mvps      float64
	hsPercent float64
}

func calcMatchAverageStats(stats []models.MatchPlayerStats) avgStats {
	if len(stats) == 0 {
		return avgStats{}
	}
	n := float64(len(stats))
	var avg avgStats
	for _, s := range stats {
		avg.kills += float64(s.Kills)
		avg.deaths += float64(s.Deaths)
		avg.damage += float64(s.Damage)
		avg.mvps += float64(s.MVPs)
		if s.Deaths > 0 {
			avg.kd += float64(s.Kills) / float64(s.Deaths)
		} else {
			avg.kd += float64(s.Kills)
		}
		if s.Kills > 0 {
			avg.hsPercent += float64(s.Headshots) / float64(s.Kills)
		}
	}
	avg.kills /= n
	avg.deaths /= n
	avg.damage /= n
	avg.kd /= n
	avg.mvps /= n
	avg.hsPercent /= n
	return avg
}

func calcPerfScore(s *models.MatchPlayerStats, avg avgStats) float64 {
	safeDiv := func(a, b float64) float64 {
		if b <= 0 {
			return 1.0
		}
		return a / b
	}

	var playerKD float64
	if s.Deaths > 0 {
		playerKD = float64(s.Kills) / float64(s.Deaths)
	} else {
		playerKD = float64(s.Kills)
	}

	var playerHS float64
	if s.Kills > 0 {
		playerHS = float64(s.Headshots) / float64(s.Kills)
	}

	killsRatio := safeDiv(float64(s.Kills), avg.kills)
	damageRatio := safeDiv(float64(s.Damage), avg.damage)
	kdRatio := safeDiv(playerKD, avg.kd)
	mvpRatio := safeDiv(float64(s.MVPs), avg.mvps)
	hsRatio := safeDiv(playerHS, avg.hsPercent)

	return killsRatio*0.30 + damageRatio*0.25 + kdRatio*0.20 + mvpRatio*0.15 + hsRatio*0.10
}
