package dashboard

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/devops/entity"
	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"
)

const (
	slaCacheMonthFormat = "2006/1"
	slaCacheMonthWindow = 13
)

type slaMonthlyLoader func(ctx context.Context, months []string) (map[string]slaInfo, error)

type slaCacheLoaders struct {
	dedicated  slaMonthlyLoader
	nextGen    slaMonthlyLoader
	serverless slaMonthlyLoader
}

type SLACache struct {
	logger       logr.Logger
	syncInterval *time.Duration

	slaDB           db.TransProvider
	serverlessSlaDB db.TransProvider

	dedicatedLoader  slaMonthlyLoader
	nextGenLoader    slaMonthlyLoader
	serverlessLoader slaMonthlyLoader

	now func() time.Time

	lock        sync.Mutex
	once        sync.Once
	initialized bool

	dedicatedData  map[string]slaInfo
	nextGenData    map[string]slaInfo
	serverlessData map[string]slaInfo
}

type slaCacheAPI interface {
	Get(ctx context.Context, months []string) (*entity.SLAInfo, error)
	Warmup(ctx context.Context) error
}

func newSLACache(
	logger logr.Logger,
	syncInterval *time.Duration,
	slaDB db.TransProvider,
	serverlessSlaDB db.TransProvider,
	loaders *slaCacheLoaders,
) *SLACache {
	if syncInterval == nil {
		defaultInterval := 15 * time.Minute
		syncInterval = &defaultInterval
	}

	cache := &SLACache{
		logger:          logger.WithName("SLACache"),
		syncInterval:    syncInterval,
		slaDB:           slaDB,
		serverlessSlaDB: serverlessSlaDB,
		now:             time.Now,
		dedicatedData:   make(map[string]slaInfo),
		nextGenData:     make(map[string]slaInfo),
		serverlessData:  make(map[string]slaInfo),
	}
	if loaders != nil {
		cache.dedicatedLoader = loaders.dedicated
		cache.nextGenLoader = loaders.nextGen
		cache.serverlessLoader = loaders.serverless
	}
	if cache.dedicatedLoader == nil {
		cache.dedicatedLoader = cache.loadDedicatedSLAMonthData
	}
	if cache.nextGenLoader == nil {
		cache.nextGenLoader = cache.loadNextGenSLAMonthData
	}
	if cache.serverlessLoader == nil {
		cache.serverlessLoader = cache.loadServerlessSLAMonthData
	}

	return cache
}

func (c *SLACache) Get(ctx context.Context, months []string) (*entity.SLAInfo, error) {
	if err := c.ensureInitialized(ctx); err != nil {
		return nil, err
	}

	c.lock.Lock()
	defer c.lock.Unlock()

	return &entity.SLAInfo{
		DedicatedSLA:  buildSLA(months, c.dedicatedData),
		NextGenSLA:    buildSLA(months, c.nextGenData),
		ServerlessSLA: buildSLA(months, c.serverlessData),
	}, nil
}

func (c *SLACache) Warmup(ctx context.Context) error {
	return c.ensureInitialized(ctx)
}

func (c *SLACache) ensureInitialized(ctx context.Context) error {
	c.lock.Lock()
	defer c.lock.Unlock()

	if c.initialized {
		return nil
	}

	if err := c.syncRecentYearLocked(ctx); err != nil {
		return err
	}
	c.initialized = true
	c.once.Do(func() {
		go c.syncScheduler()
	})
	return nil
}

func (c *SLACache) syncCurrentMonth(ctx context.Context) error {
	c.lock.Lock()
	defer c.lock.Unlock()

	if !c.initialized {
		if err := c.syncRecentYearLocked(ctx); err != nil {
			return err
		}
		c.initialized = true
		return nil
	}

	return c.syncCurrentMonthLocked(ctx)
}

func (c *SLACache) syncScheduler() {
	syncTicker := time.NewTicker(*c.syncInterval)
	for {
		<-syncTicker.C
		if err := c.syncCurrentMonth(context.Background()); err != nil {
			c.logger.Error(err, "sync current month SLA failed")
		}
	}
}

func (c *SLACache) syncRecentYearLocked(ctx context.Context) error {
	months := c.recentMonths()
	c.logger.Info("syncing SLA cache", "months", months)

	dedicatedData, err := c.dedicatedLoader(ctx, months)
	if err != nil {
		return err
	}
	nextGenData, err := c.nextGenLoader(ctx, months)
	if err != nil {
		return err
	}
	serverlessData, err := c.serverlessLoader(ctx, months)
	if err != nil {
		return err
	}

	c.dedicatedData = dedicatedData
	c.nextGenData = nextGenData
	c.serverlessData = serverlessData

	c.logger.Info("sync SLA cache done", "months", len(months))
	return nil
}

func (c *SLACache) syncCurrentMonthLocked(ctx context.Context) error {
	currentMonth := c.currentMonth()
	months := []string{currentMonth}

	dedicatedData, err := c.dedicatedLoader(ctx, months)
	if err != nil {
		return err
	}
	nextGenData, err := c.nextGenLoader(ctx, months)
	if err != nil {
		return err
	}
	serverlessData, err := c.serverlessLoader(ctx, months)
	if err != nil {
		return err
	}

	c.updateCurrentMonth(c.dedicatedData, currentMonth, dedicatedData)
	c.updateCurrentMonth(c.nextGenData, currentMonth, nextGenData)
	c.updateCurrentMonth(c.serverlessData, currentMonth, serverlessData)
	c.pruneExpiredMonths()

	c.logger.Info("sync current month SLA cache done", "month", currentMonth)
	return nil
}

func (c *SLACache) updateCurrentMonth(cache map[string]slaInfo, currentMonth string, loaded map[string]slaInfo) {
	if item, ok := loaded[currentMonth]; ok {
		cache[currentMonth] = item
		return
	}
	delete(cache, currentMonth)
}

func (c *SLACache) pruneExpiredMonths() {
	allowed := make(map[string]struct{}, slaCacheMonthWindow)
	for _, month := range c.recentMonths() {
		allowed[month] = struct{}{}
	}
	pruneMonthData(c.dedicatedData, allowed)
	pruneMonthData(c.nextGenData, allowed)
	pruneMonthData(c.serverlessData, allowed)
}

func (c *SLACache) recentMonths() []string {
	currentMonth, err := time.Parse(slaCacheMonthFormat, c.currentMonth())
	if err != nil {
		return nil
	}
	startMonth := currentMonth.AddDate(0, -(slaCacheMonthWindow - 1), 0)
	return getMonthsFromTimestamp(startMonth.Unix(), currentMonth.Unix(), slaCacheMonthFormat)
}

func (c *SLACache) currentMonth() string {
	return c.now().Format(slaCacheMonthFormat)
}

func pruneMonthData(data map[string]slaInfo, allowed map[string]struct{}) {
	for month := range data {
		if _, ok := allowed[month]; ok {
			continue
		}
		delete(data, month)
	}
}

type slaInfo struct {
	Month   string
	Perfect int
	Warning int
	Breach  int
}

func buildSLA(months []string, monthToData map[string]slaInfo) *entity.SLA {
	trends := make([]entity.SeriesMonthlyTrend, 0, len(months)*3)
	for _, month := range months {
		item, ok := monthToData[month]
		if !ok {
			continue
		}
		trends = append(trends, entity.SeriesMonthlyTrend{
			Month:  item.Month,
			Series: "perfect",
			Value:  item.Perfect,
		})
		trends = append(trends, entity.SeriesMonthlyTrend{
			Month:  item.Month,
			Series: "warning",
			Value:  item.Warning,
		})
		trends = append(trends, entity.SeriesMonthlyTrend{
			Month:  item.Month,
			Series: "breach",
			Value:  item.Breach,
		})
	}

	current := float64(0)
	if len(months) > 0 {
		if latest, ok := monthToData[months[len(months)-1]]; ok {
			total := latest.Perfect + latest.Warning + latest.Breach
			if total > 0 {
				current = float64(latest.Perfect+latest.Warning) / float64(total)
			}
		}
	}

	return &entity.SLA{MonthlyTrends: trends, Current: current}
}

func (c *SLACache) loadDedicatedSLAMonthData(ctx context.Context, months []string) (map[string]slaInfo, error) {
	rdb := c.slaDB.ReadDB(ctx)
	var data []slaInfo
	err := rdb.Raw(`
	SELECT
		month,
		COUNT(IF(status='perfect', 1, NULL)) AS perfect,
		COUNT(IF(status='warning', 1, NULL)) AS warning,
		COUNT(IF(status='breach', 1, NULL)) AS breach
	FROM sla
	WHERE month IN ? AND tenant_kind='external'
    AND cluster_deploy_type NOT IN ('premium', 'byoc')
	GROUP BY month`, months).Scan(&data).Error
	if err != nil {
		return nil, fmt.Errorf("get dedicated sla info failed: %w", err)
	}
	monthToData := make(map[string]slaInfo)
	for _, item := range data {
		monthToData[item.Month] = item
	}
	return monthToData, nil
}

func (c *SLACache) loadNextGenSLAMonthData(ctx context.Context, months []string) (map[string]slaInfo, error) {
	rdb := c.slaDB.ReadDB(ctx)
	var data []slaInfo
	err := rdb.Raw(`
	SELECT
		month,
		COUNT(IF(status='perfect', 1, NULL)) AS perfect,
		COUNT(IF(status='warning', 1, NULL)) AS warning,
		COUNT(IF(status='breach', 1, NULL)) AS breach
	FROM sla
	WHERE month IN ? AND tenant_kind='external'
    AND cluster_deploy_type IN ('premium', 'byoc')
	GROUP BY month`, months).Scan(&data).Error
	if err != nil {
		return nil, fmt.Errorf("get next-gen sla info failed: %w", err)
	}
	monthToData := make(map[string]slaInfo)
	for _, item := range data {
		monthToData[item.Month] = item
	}
	return monthToData, nil
}

type downTime struct {
	ClusterID string
	DownRate  float64
	Month     string
}

func (c *SLACache) loadServerlessSLAMonthData(ctx context.Context, months []string) (map[string]slaInfo, error) {
	dbMonths := make([]string, 0, len(months))
	dbMonthToDisplayMonth := make(map[string]string, len(months))
	for _, month := range months {
		dbMonth, err := convertDateFormat(month, "2006/1", "2006-01")
		if err != nil {
			return nil, fmt.Errorf("convert serverless month format failed: %w", err)
		}
		dbMonths = append(dbMonths, dbMonth)
		dbMonthToDisplayMonth[dbMonth] = month
	}

	var data []downTime
	rdb := c.serverlessSlaDB.ReadDB(ctx)
	err := rdb.Raw("SELECT cluster_id, month, down_rate FROM downtimes WHERE down_rate=1 and month IN ?", dbMonths).Scan(&data).Error
	if err != nil {
		return nil, fmt.Errorf("get serverless downtime failed: %w", err)
	}

	downtime := make(map[string]map[string]int64)
	for _, item := range data {
		if _, ok := downtime[item.Month]; !ok {
			downtime[item.Month] = make(map[string]int64)
		}
		if _, ok := downtime[item.Month][item.ClusterID]; !ok {
			downtime[item.Month][item.ClusterID] = 1
		} else {
			downtime[item.Month][item.ClusterID]++
		}
	}

	result := make(map[string]slaInfo, len(months))
	for _, month := range dbMonths {
		minutes, aliveCluster, err := getMonthMinutesAndClusters(month, rdb)
		if err != nil {
			return nil, fmt.Errorf("get serverless month info failed: %w", err)
		}

		clusterCount := len(aliveCluster)
		clusters := downtime[month]
		met, missed := 0, 0
		for id, d := range clusters {
			if _, ok := aliveCluster[id]; !ok {
				continue
			}
			sla := 1 - float64(d)/float64(minutes)
			if sla < 0.999 {
				missed++
			} else {
				met++
			}
		}

		displayMonth := dbMonthToDisplayMonth[month]
		result[displayMonth] = slaInfo{
			Month:   displayMonth,
			Perfect: clusterCount - met - missed,
			Warning: met,
			Breach:  missed,
		}
	}
	return result, nil
}

func getMonthMinutesAndClusters(month string, rdb *gorm.DB) (int64, map[string]bool, error) {
	firstDay, err := time.Parse("2006-01", month)
	if err != nil {
		return 0, nil, err
	}
	lastDay := firstDay.AddDate(0, 1, 0)
	minutes := lastDay.Sub(firstDay).Minutes()

	var clusterID []string
	err = rdb.Raw(
		`SELECT id FROM clusters 
				WHERE created_time < FROM_UNIXTIME(?)
				AND is_paid=1
			`, lastDay.Unix()).Scan(&clusterID).Error
	if err != nil {
		return 0, nil, err
	}
	cluster := make(map[string]bool)
	for _, id := range clusterID {
		cluster[id] = true
	}

	return int64(minutes), cluster, nil
}
