package repositories

import (
	"astrology-api/dto"
	"math"
	"sort"
	"strings"
)

// The website home page's top astrologers: highest rated, most attended.
//
// Attended sessions are counted from both flows: the legacy chatrequest /
// callrequest tables (status Completed) and consultations (COMPLETED with
// time on the clock). The new flow never writes the legacy tables, so nothing
// is counted twice. consultations is read separately and an error there is
// ignored, so a database without that table still ranks on the rest.

// reviewPriorWeight is how many "average" reviews every astrologer starts
// with. Without it one 5-star review outranks five hundred 4.9s.
const reviewPriorWeight = 5.0

type topAstrologerRow struct {
	ID            uint    `gorm:"column:id"`
	Name          string  `gorm:"column:name"`
	DisplayName   string  `gorm:"column:displayName"`
	ProfileImage  string  `gorm:"column:profileImage"`
	Experience    float64 `gorm:"column:experience"`
	PrimarySkill  string  `gorm:"column:primarySkill"`
	AllSkill      string  `gorm:"column:allSkill"`
	LanguageKnown string  `gorm:"column:languageKnown"`
	ChatRate      int     `gorm:"column:chatRate"`
	CallRate      int     `gorm:"column:audioCallRate"`
	VideoCallRate int     `gorm:"column:videoCallRate"`
	Status        string  `gorm:"column:status"`
	AverageRating float64 `gorm:"column:averageRating"`
	RatingCount   int64   `gorm:"column:ratingCount"`
	ChatOrders    int64   `gorm:"column:chatOrders"`
	CallOrders    int64   `gorm:"column:callOrders"`
}

func (r *astrologerRepository) GetTopAstrologers(limit int) ([]dto.TopAstrologerResponse, error) {

	reviews := r.db.
		Table("user_reviews").
		Select("astrologerId, AVG(rating) AS averageRating, COUNT(id) AS ratingCount").
		Where("astrologerId > 0").
		Where("isDelete = ?", 0).
		Where("isActive = ?", 1).
		Group("astrologerId")

	var rows []topAstrologerRow

	// Same visibility rule as the customer astrologer list.
	err := r.db.
		Table("astrologers AS a").
		Select(`
			a.id, a.name, a.displayName, a.profileImage, a.experience,
			a.primarySkill, a.allSkill, a.languageKnown,
			a.chatRate, a.audioCallRate, a.videoCallRate, a.status,
			IFNULL(rv.averageRating, 0) AS averageRating,
			IFNULL(rv.ratingCount, 0)   AS ratingCount,
			IFNULL(ch.chatOrders, 0)    AS chatOrders,
			IFNULL(cl.callOrders, 0)    AS callOrders
		`).
		Joins("LEFT JOIN (?) AS rv ON rv.astrologerId = a.id", reviews).
		Joins("LEFT JOIN (?) AS ch ON ch.astrologerId = a.id", r.chatSummarySubQuery()).
		Joins("LEFT JOIN (?) AS cl ON cl.astrologerId = a.id", r.callSummarySubQuery()).
		Where("a.isActive = ?", 1).
		Where("a.isDelete = ?", 0).
		Where("a.isVerified = ?", 1).
		Scan(&rows).Error

	if err != nil {
		return nil, err
	}

	r.addConsultationOrders(rows)

	//------------------------------------------------
	// Rank
	//------------------------------------------------
	//
	// Primary: the rating, pulled toward the overall average in proportion
	// to how few reviews it rests on. Then attended sessions, then review
	// count, then the older profile.

	var weighted, reviewsTotal float64

	for _, row := range rows {
		weighted += row.AverageRating * float64(row.RatingCount)
		reviewsTotal += float64(row.RatingCount)
	}

	mean := 0.0

	if reviewsTotal > 0 {
		mean = weighted / reviewsTotal
	}

	score := func(row topAstrologerRow) float64 {
		count := float64(row.RatingCount)
		return (count*row.AverageRating + reviewPriorWeight*mean) / (count + reviewPriorWeight)
	}

	sort.SliceStable(rows, func(i, j int) bool {

		a, b := rows[i], rows[j]

		if sa, sb := round2(score(a)), round2(score(b)); sa != sb {
			return sa > sb
		}

		if ta, tb := a.ChatOrders+a.CallOrders, b.ChatOrders+b.CallOrders; ta != tb {
			return ta > tb
		}

		if a.RatingCount != b.RatingCount {
			return a.RatingCount > b.RatingCount
		}

		return a.ID < b.ID
	})

	if len(rows) > limit {
		rows = rows[:limit]
	}

	response := make([]dto.TopAstrologerResponse, 0, len(rows))

	for index, row := range rows {

		name := row.Name

		if strings.TrimSpace(row.DisplayName) != "" {
			name = row.DisplayName
		}

		primarySkill, allSkill := r.resolveSkillNames(row.ID, row.PrimarySkill, row.AllSkill)

		status := strings.ToUpper(strings.TrimSpace(row.Status))

		if status == "" {
			status = "OFFLINE"
		}

		response = append(response, dto.TopAstrologerResponse{
			Rank:          index + 1,
			ID:            row.ID,
			Name:          name,
			ProfileImage:  row.ProfileImage,
			Experience:    row.Experience,
			PrimarySkill:  primarySkill,
			AllSkill:      allSkill,
			LanguageKnown: r.resolveLanguageNames(row.ID, row.LanguageKnown),
			ChatRate:      row.ChatRate,
			CallRate:      row.CallRate,
			VideoCallRate: row.VideoCallRate,
			Rating:        math.Round(row.AverageRating*10) / 10,
			RatingCount:   row.RatingCount,
			ChatOrders:    row.ChatOrders,
			CallOrders:    row.CallOrders,
			TotalOrders:   row.ChatOrders + row.CallOrders,
			Status:        status,
			IsOnline:      status == "ONLINE",
			IsBusy:        status == "BUSY",
		})
	}

	return response, nil
}

// addConsultationOrders adds the new flow's attended sessions. An error (for
// instance no consultations table yet) leaves the legacy counts as they are.
func (r *astrologerRepository) addConsultationOrders(rows []topAstrologerRow) {

	var counts []struct {
		AstrologerID uint   `gorm:"column:astrologerId"`
		Medium       string `gorm:"column:medium"`
		Total        int64  `gorm:"column:total"`
	}

	err := r.db.
		Table("consultations").
		Select("astrologerId, medium, COUNT(id) AS total").
		Where("status = ?", "COMPLETED").
		Where("durationSeconds > ?", 0).
		Where("isDelete = ?", 0).
		Group("astrologerId, medium").
		Scan(&counts).Error

	if err != nil {
		return
	}

	index := make(map[uint]int, len(rows))

	for i, row := range rows {
		index[row.ID] = i
	}

	for _, count := range counts {

		i, ok := index[count.AstrologerID]

		if !ok {
			continue
		}

		if count.Medium == "CHAT" {
			rows[i].ChatOrders += count.Total
		} else {
			rows[i].CallOrders += count.Total
		}
	}
}

func round2(value float64) float64 {
	return math.Round(value*100) / 100
}
