package v1

import (
	"math"
	"net/http"
	"strings"
	"time"

	"github.com/duke-git/lancet/v2/slice"
	"github.com/go-chi/chi/v5"
	"github.com/muety/wakapi/helpers"
	"github.com/muety/wakapi/middlewares"
	"github.com/muety/wakapi/models"
	"github.com/muety/wakapi/utils"

	conf "github.com/muety/wakapi/config"
	v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
	"github.com/muety/wakapi/services"
)

type LeadersHandler struct {
	config          *conf.Config
	userSrvc        services.IUserService
	leaderboardSrvc services.ILeaderboardService
}

func NewLeadersHandler(userService services.IUserService, leaderboardService services.ILeaderboardService) *LeadersHandler {
	return &LeadersHandler{
		userSrvc:        userService,
		leaderboardSrvc: leaderboardService,
		config:          conf.Get(),
	}
}

func (h *LeadersHandler) RegisterRoutes(router chi.Router) {
	router.Group(func(r chi.Router) {
		r.Use(middlewares.NewAuthenticateMiddleware(h.userSrvc).WithOptionalFor("/").Handler)
		r.Get("/compat/wakatime/v1/leaders", h.Get)
	})
}

// @Summary List of users ranked by coding activity in descending order.
// @Description Mimics https://wakatime.com/developers#leaders
// @ID get-wakatime-leaders
// @Tags wakatime
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} v1.LeadersViewModel
// @Router /compat/wakatime/v1/leaders [get]
func (h *LeadersHandler) Get(w http.ResponseWriter, r *http.Request) {
	user := middlewares.GetPrincipal(r)
	languageParam := strings.ToLower(r.URL.Query().Get("language"))
	pageParams := utils.ParsePageParamsWithDefault(r, 1, 100)
	by := models.SummaryLanguage

	loadPrimaryLeaderboard := func() (models.Leaderboard, error) {
		if languageParam == "" {
			return h.leaderboardSrvc.GetByInterval(h.leaderboardSrvc.GetDefaultScope(), pageParams, true)
		} else {
			l, err := h.leaderboardSrvc.GetAggregatedByInterval(h.leaderboardSrvc.GetDefaultScope(), &by, pageParams, true)
			if err == nil {
				return l.TopByKey(by, languageParam), err
			}
			return nil, err
		}
	}

	loadPrimaryUserLeaderboard := func() (models.Leaderboard, error) {
		if user == nil {
			return []*models.LeaderboardItemRanked{}, nil
		}
		if languageParam == "" {
			return h.leaderboardSrvc.GetByIntervalAndUser(h.leaderboardSrvc.GetDefaultScope(), user.ID, true)
		} else {
			l, err := h.leaderboardSrvc.GetAggregatedByIntervalAndUser(h.leaderboardSrvc.GetDefaultScope(), user.ID, &by, true)
			if err == nil {
				return l.TopByKey(by, languageParam), err
			}
			return nil, err
		}
	}

	primaryLeaderboard, err := loadPrimaryLeaderboard()
	if err != nil {
		conf.Log().Request(r).Error("error while fetching general leaderboard items", "error", err)
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("something went wrong"))
		return
	}
	primaryLeaderboard.FilterEmpty()

	languageLeaderboard, err := h.leaderboardSrvc.GetAggregatedByInterval(h.leaderboardSrvc.GetDefaultScope(), &by, &utils.PageParams{Page: 1, PageSize: math.MaxUint16}, true)
	if err != nil {
		conf.Log().Request(r).Error("error while fetching language-specific leaderboard items", "error", err)
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("something went wrong"))
		return
	}

	// regardless of page, always show own rank
	if user != nil && !primaryLeaderboard.HasUser(user.ID) {
		if l, err := loadPrimaryUserLeaderboard(); err == nil {
			primaryLeaderboard.AddMany(l)
		} else {
			conf.Log().Request(r).Error("error while fetching own general user leaderboard", "userID", user.ID, "error", err)
		}
		// no need to fetch language-leaderboard for user, because not using pagination above
	}

	vm := h.buildViewModel(primaryLeaderboard, languageLeaderboard, user, h.leaderboardSrvc.GetDefaultScope(), pageParams)
	vm.Language = languageParam
	helpers.RespondJSON(w, r, http.StatusOK, vm)
}

func (h *LeadersHandler) buildViewModel(globalLeaderboard, languageLeaderboard models.Leaderboard, user *models.User, interval *models.IntervalKey, pageParams *utils.PageParams) *v1.LeadersViewModel {
	var currentUserGlobal []*models.LeaderboardItemRanked
	if user != nil {
		currentUserGlobal = *globalLeaderboard.GetByUser(user.ID)
	}

	totalUsers, _ := h.leaderboardSrvc.CountUsers(true)
	totalPages := int(totalUsers/int64(pageParams.PageSize) + 1)

	_, from, to := helpers.ResolveIntervalTZ(interval, time.UTC, time.Monday)
	numDays := len(utils.SplitRangeByDays(from, to))

	vm := &v1.LeadersViewModel{
		Data:       make([]*v1.LeadersEntry, 0, len(languageLeaderboard.UserIDs())),
		Page:       pageParams.Page,
		TotalPages: totalPages,
		Range: &v1.LeadersRange{
			EndText:   helpers.FormatDateHuman(to),
			EndDate:   to.Format(time.RFC3339),
			StartText: helpers.FormatDateHuman(from),
			StartDate: from.Format(time.RFC3339),
			Name:      (*interval)[0],
			Text:      interval.GetHumanReadable(),
		},
	}

	if len(currentUserGlobal) > 0 {
		vm.CurrentUser = &v1.LeadersCurrentUser{
			Rank: int(currentUserGlobal[0].Rank),
			Page: 1,
			User: v1.NewFromUser(currentUserGlobal[0].User),
		}
	}

	for _, entry := range globalLeaderboard {
		dailyAverage := entry.Total / time.Duration(numDays)

		vm.Data = append(vm.Data, &v1.LeadersEntry{
			Rank: int(entry.Rank),
			RunningTotal: &v1.LeadersRunningTotal{
				TotalSeconds:              float64(entry.Total / time.Second),
				HumanReadableTotal:        helpers.FmtWakatimeDuration(entry.Total),
				DailyAverage:              float64(dailyAverage / time.Second),
				HumanReadableDailyAverage: helpers.FmtWakatimeDuration(dailyAverage),
				Languages: slice.Map[models.LeaderboardKeyTotal, *v1.LeadersLanguage](languageLeaderboard.TopKeysTotalsByUser(models.SummaryLanguage, entry.UserID), func(i int, item models.LeaderboardKeyTotal) *v1.LeadersLanguage {
					return &v1.LeadersLanguage{
						Name:         item.Key,
						TotalSeconds: float64(item.Total / time.Second),
					}
				}),
			},
			User: v1.NewFromUser(entry.User),
		})
	}

	return vm
}
