// Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0.

package keyviz

import (
	"context"
	"fmt"
	"math"
	"net/http"
	"strconv"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/ngm/keyviz/datasource"
	"github.com/pingcap-inc/clinic/pkg/ngm/keyviz/keyvisual/decorator"
	"github.com/pingcap-inc/clinic/pkg/ngm/keyviz/keyvisual/matrix"
	"github.com/pingcap-inc/clinic/pkg/ngm/keyviz/keyvisual/region"
	kstorage "github.com/pingcap-inc/clinic/pkg/ngm/keyviz/keyvisual/storage"
	"github.com/pingcap-inc/clinic/pkg/ngm/storage"
	"github.com/pingcap-inc/clinic/pkg/ngm/utils"
)

const (
	DefaultTimeout = time.Second * 180

	heatmapsMaxDisplayY   = 1536
	distanceStrategyRatio = 1.0 / math.Phi
	distanceStrategyLevel = 15
	distanceStrategyCount = 50
)

var (
	defaultStatConfig = kstorage.StatConfig{
		LayersConfig: []kstorage.LayerConfig{
			{Len: 60, Ratio: 2 / 1},                     // step 1 minutes, total 60, 1 hours (sum: 1 hours)
			{Len: 60 / 2 * 7, Ratio: 6 / 2},             // step 2 minutes, total 210, 7 hours (sum: 8 hours)
			{Len: 60 / 6 * 16, Ratio: 30 / 6},           // step 6 minutes, total 160, 16 hours (sum: 1 days)
			{Len: 60 / 30 * 24 * 6, Ratio: 4 * 60 / 30}, // step 30 minutes, total 288, 6 days (sum: 1 weeks)
			{Len: 24 / 4 * 28, Ratio: 0},                // step 4 hours, total 168, 4 weeks (sum: 5 weeks)
		},
	}
	distanceSplitStrategy = matrix.DistanceSplitStrategy(
		distanceStrategyRatio,
		distanceStrategyLevel,
		distanceStrategyCount,
	)
)

func init() {
	matrix.StartWorkers(context.Background(), &sync.WaitGroup{}, distanceSplitStrategy)
}

func InitKeyvizApi(g *gin.RouterGroup, s *Service) {
	g.GET("/keyvisual/config", s.Config)
	g.GET("/keyvisual/heatmaps", s.Heatmaps)
}

type Service struct {
	Env     string
	factory storage.StorageFactory
}

func NewService(env string, stores storage.StorageFactory) *Service {
	return &Service{
		Env:     env,
		factory: stores,
	}
}

type ClusterHeader struct {
	Provider   string `header:"x-provider" binding:"required"`
	Region     string `header:"x-region" binding:"required"`
	TenantID   string `header:"x-org-id"`
	ProjectID  string `header:"x-project-id"`
	ClusterID  string `header:"x-cluster-id" binding:"required"`
	DeployType string `header:"x-deploy-type"` // "dedicated" or "shared"
	Env        string `header:"x-env"`         // Use default env if emprty
}

type KeyVisualConfig struct {
	AutoCollectionDisabled bool   `json:"auto_collection_disabled"`
	Policy                 string `json:"policy"`
	PolicyKVSeparator      string `json:"policy_kv_separator"`
}

// @Summary		Key Visual Config
// @Description	Mock config for keyvisual
// @Success		200	{object}	KeyVisualConfig
// @Router			/keyvisual/config [get]
func (s *Service) Config(c *gin.Context) {
	c.JSON(http.StatusOK, KeyVisualConfig{
		AutoCollectionDisabled: false,
		Policy:                 "db",
		PolicyKVSeparator:      "",
	})
}

// @Summary		Key Visual Heatmaps
// @Description	Heatmaps in a given range to visualize TiKV usage
// @Param			startkey	query		string	false	"The start of the key range"
// @Param			endkey		query		string	false	"The end of the key range"
// @Param			starttime	query		int		false	"The start of the time range (Unix)"
// @Param			endtime		query		int		false	"The end of the time range (Unix)"
// @Param			type		query		string	false	"Main types of data"	Enums(written_bytes, read_bytes, written_keys, read_keys, integration)
// @Success		200			{object}	k.Matrix
// @Router			/keyvisual/heatmaps [get]
func (s *Service) Heatmaps(c *gin.Context) {
	var header ClusterHeader
	if err := c.ShouldBindHeader(&header); err != nil {
		c.JSON(http.StatusBadRequest, "bad request")
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), DefaultTimeout)
	defer cancel()

	if s.Env == "prod" && header.Region == "ap-northeast-1" {
		o11yURL := fmt.Sprintf("http://www.ds.%s.%s.observability.tidbcloud.com/internal/diagnosis/api/v1/keyvisual/heatmaps", header.Region, header.Provider)
		utils.ProxyO11yURL(ctx, c, nil, o11yURL, s.Env, "", nil)
		return
	}

	startKey := c.Query("startkey")
	endKey := c.Query("endkey")
	startTimeString := c.Query("starttime")
	endTimeString := c.Query("endtime")
	typ := c.Query("type")

	endTime := time.Now().Truncate(time.Minute)
	startTime := endTime.Add(-360 * time.Minute)
	if startTimeString != "" {
		tsSec, err := strconv.ParseInt(startTimeString, 10, 64)
		if err != nil {
			log.Error("parse ts failed", zap.Error(err))
			c.JSON(http.StatusBadRequest, "bad request")
			return
		}
		tsSec -= tsSec % 60
		startTime = time.Unix(tsSec, 0)
	}
	if endTimeString != "" {
		tsSec, err := strconv.ParseInt(endTimeString, 10, 64)
		if err != nil {
			log.Error("parse ts failed", zap.Error(err))
			c.JSON(http.StatusBadRequest, "bad request")
			return
		}
		tsSec -= tsSec % 60
		endTime = time.Unix(tsSec, 0)
	}
	if !(startTime.Before(endTime) && (endKey == "" || startKey < endKey)) {
		c.JSON(http.StatusBadRequest, "bad request")
		return
	}

	log.Debug("Request matrix",
		zap.Time("start-time", startTime),
		zap.Time("end-time", endTime),
		zap.String("start-key", startKey),
		zap.String("end-key", endKey),
		zap.String("type", typ),
	)

	store, err := s.factory.Storage(c.Request.Context(), header.Provider, header.Region)
	if err != nil {
		c.JSON(http.StatusBadRequest, "bad request")
		return
	}
	env := header.Env
	if env == "" {
		env = s.Env
	}

	m, err := Heatmaps(c.Request.Context(), env, header, startTime, endTime, startKey, endKey, typ, store)
	if err != nil {
		log.Error("failed to query heatmaps", zap.Error(err))
		c.JSON(http.StatusBadRequest, "failed to query heatmaps")
		return
	}
	c.JSON(http.StatusOK, m)
}

func Heatmaps(
	ctx context.Context,
	env string,
	header ClusterHeader,
	startTime, endTime time.Time,
	startKey, endKey string,
	typ string,
	store storage.Storage,
) (*matrix.Matrix, error) {
	tablesInfo, err := datasource.FetchDBTablesInfo(ctx,
		store, env, header.Provider, header.Region, header.TenantID, header.ProjectID, header.ClusterID)
	if err != nil {
		return nil, err
	}
	regionsInfo, err := datasource.FetchRegionsInfo(ctx,
		store, env, header.Provider, header.Region, header.TenantID, header.ProjectID, header.ClusterID, startTime, endTime)
	if err != nil {
		return nil, err
	}
	db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"))
	if err != nil {
		return nil, err
	}
	defer func() {
		inner, err := db.DB()
		if err != nil {
			log.Warn("failed to get inner sqlite db", zap.Error(err))
		}
		if err := inner.Close(); err != nil {
			log.Warn("failed to close sqlite db", zap.Error(err))
		}
	}()
	strategy := &matrix.Strategy{
		LabelStrategy: decorator.TiDBWithoutClientLabelStrategy(tablesInfo),
		SplitStrategy: distanceSplitStrategy,
	}
	stat := kstorage.NewStat(db, defaultStatConfig, strategy, startTime)
	if err := stat.Restore(); err != nil {
		return nil, err
	}
	for _, r := range regionsInfo {
		stat.Append(&r.Info, r.Time)
	}
	tag := region.IntoTag(typ)
	plane := stat.Range(startTime, endTime, startKey, endKey, tag)
	resp := plane.Pixel(strategy, heatmapsMaxDisplayY, region.GetDisplayTags(tag))
	resp.Range(startKey, endKey)
	resp.DataMap = map[string][][]uint64{
		typ: resp.DataMap[typ],
	}
	return &resp, nil
}
