package main

import (
	"crypto/tls"
	"database/sql"
	"encoding/base64"
	"errors"
	"fmt"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-sql-driver/mysql"
	_ "github.com/go-sql-driver/mysql"
	"github.com/robfig/cron/v3"
	"github.com/twinj/uuid"
	"github.com/uptrace/opentelemetry-go-extra/otelzap"
	"go.uber.org/zap"

	. "github.com/tidbcloud/devtier-store-service/pkg/billing"
	. "github.com/tidbcloud/devtier-store-service/utils"
)

func FatchServerlessDailyCost() {
	logger := otelzap.L().With(zap.String("trace_id", uuid.NewV1().String()))
	logger.Info("FatchServerlessDailyCost job start...")
	defer logger.Info("FatchServerlessDailyCost job end...")
	for i := 0; i < 3; i++ {
		err := processer(logger)
		if err == nil {
			break
		}
		logger.Error("processer failed, retry it", zap.Any("retry", i), zap.Error(err))
		time.Sleep(time.Duration(60) * time.Second)
	}
}

func processer(logger *zap.Logger) (err error) {
	defer func() {
		if err1 := recover(); err1 != nil {
			logger.Error("processer panic exception", zap.Any("error", err1))
			for i := 1; ; i++ {
				pc, file, line, ok := runtime.Caller(i)
				if !ok {
					break
				}
				logger.Error("panic recover", zap.Any("pointer", pc), zap.Any(file, line))
			}
		}
	}()

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		logger := otelzap.L().With(zap.String("trace_id", uuid.NewV1().String()))
		logger.Info("FatchServerlessDailyCost updateServerlessDb job start...")
		defer logger.Info("FatchServerlessDailyCost updateServerlessDb job end...")
		updateServerlessDb(logger, 4) // T-1 bills
		updateServerlessDb(logger, 2) // T-2 bills
		updateServerlessDb(logger, 1) // T-4 bills, update
	}()

	date, cost := GetAwsServerlessCostFromAthena(logger)
	if len(date) == 0 || len(cost) == 0 {
		logger.Error("get athena cost data failed", zap.Any(date, cost))
		return errors.New("get athena cost data failed")
	}
	logger = logger.With(zap.Any("date", date))
	logger.Info("get athena cost success", zap.Any("cost", cost))

	aClsNum := GetServerlessClusterNumFromMetrics(logger, date, "1d", true)
	clsNum := GetServerlessClusterNumFromMetrics(logger, date, "1d", false)
	tClsNum := GetServerlessClusterNumFromGCP(logger, date)
	logger.Info("get metric result, [gcp] total_cls_num=%d, [o11y]",
		zap.Any("cls_num", tClsNum), zap.Any("cls_num", clsNum), zap.Any("active_cls_num", aClsNum))

	err = S3UpdateObject(logger, date, cost, clsNum, aClsNum, tClsNum)
	logger.Info("update s3. (data:%s,%s,%d,%s,%s), err=%v", zap.Any(date, cost),
		zap.Any("cls_num", tClsNum), zap.Any("cls_num", clsNum), zap.Any("active_cls_num", aClsNum), zap.Error(err))

	wg.Wait()
	return
}

func updateServerlessDb(logger *zap.Logger, offset uint32) {
	//logger := otelzap.L().Ctx(ctx).Logger().With(zap.String("func", "updateServerlessDb"))
	regionCost := GetAwsServerlessCostByRegion(logger, offset)
	logger.Info("get cost by region ", zap.Any("region_cost", regionCost))
	regionCluster, _ := GetServerlessClusterByRegion(logger)
	logger.Info("get cluster by region ", zap.Any("region_clusters", regionCluster))
	for id, data := range regionCost {
		regionCost[id].ClusterNum = regionCluster[data.Region]
		if regionCost[id].ClusterNum == 0 {
			regionCost[id].ClusterNum = 1
		}
	}
	logger.Info("update cost with cluster num ", zap.Any("region_cost", regionCost))
	mysql.RegisterTLSConfig("tidb", &tls.Config{
		MinVersion: tls.VersionTLS12,
		ServerName: "gateway01.us-west-2.prod.aws.tidbcloud.com",
	})
	sEnc := "eGczNGUyUGlLcGx2RVhSYQ=="
	sDec, _ := base64.StdEncoding.DecodeString(sEnc)
	dsn := "3zVt5C6dwEUJdNC.root:" + string(sDec) + "@tcp(gateway01.us-west-2.prod.aws.tidbcloud.com:4000)/serverless_tier?tls=tidb"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		logger.Error("failed to connect database ", zap.Error(err))
	}
	defer db.Close()
	paidCostMap := make(map[string]float64)
	rows, err := db.Query("select provider, region, sum(cost) as cost from serverless_tier_paid_daily where start_day <= ? and ? <= end_day group by provider, region", regionCost[0].Day, regionCost[0].Day)
	if err != nil {
		logger.Error("query serverless_tier_paid_daily table failed", zap.Error(err))
	} else {
		for rows.Next() {
			var paidCost ServerlessTierPaidUser
			rows.Scan(&paidCost.Provider, &paidCost.Region, &paidCost.Cost)
			paidCostMap[paidCost.Provider+paidCost.Region] = paidCost.Cost
		}
		logger.Info("query serverless_tier_paid_daily table, day=%v, results=%v", zap.Any(regionCost[0].Day, paidCostMap))
	}

	for _, v := range regionCost {
		ServerlessDailyOpsCost.WithLabelValues(v.Provider, v.Region, fmt.Sprint(offset)).Set(v.Cost)
		ServerlessDailyClusters.WithLabelValues(v.Provider, v.Region, "total").Set(float64(v.ClusterNum))
		if _, err := db.Exec("replace into serverless_billing_daily (day, provider, region, cost, cluster_num, cluster_cost_30d) values(?,?,?,?,?,?)",
			v.Day, v.Provider, v.Region, v.Cost, v.ClusterNum, v.Cost/float64(v.ClusterNum)*30); err != nil {
			logger.Error("update database failed", zap.Error(err))
		} else {
			logger.Info("update serverless_billing_daily for total ", zap.Any("value", v))
		}
		if minusCost, ok := paidCostMap[v.Provider+v.Region]; ok {
			v.Cost = v.Cost - minusCost
		}
		if _, err := db.Exec("replace into serverless_billing_free_cluster_daily (day, provider, region, cost, cluster_num, cluster_cost_30d) values(?,?,?,?,?,?)",
			v.Day, v.Provider, v.Region, v.Cost, v.ClusterNum, v.Cost/float64(v.ClusterNum)*30); err != nil {
			logger.Error("update database failed", zap.Error(err))
		} else {
			logger.Info("update serverless_billing_daily for free ", zap.Any("value", v))
		}
	}
}

func getDB() (*sql.DB, error) {
	mysql.RegisterTLSConfig("tidb", &tls.Config{
		MinVersion: tls.VersionTLS12,
		ServerName: "gateway01.us-west-2.prod.aws.tidbcloud.com",
	})
	sEnc := "eGczNGUyUGlLcGx2RVhSYQ=="
	sDec, _ := base64.StdEncoding.DecodeString(sEnc)
	dsn := "3zVt5C6dwEUJdNC.root:" + string(sDec) + "@tcp(gateway01.us-west-2.prod.aws.tidbcloud.com:4000)/serverless_tier?tls=tidb"
	return sql.Open("mysql", dsn)
}

func FatchCloudTenants() {
	date := time.Now().AddDate(0, 0, -3).Format("2006-01-02")
	logger := otelzap.L().With(zap.String("trace_id", uuid.NewV1().String()), zap.Any(">= date", date))
	logger.Info("FatchCloudTenants job start...")
	defer logger.Info("FatchCloudTenants job end...")

	db, err := getDB()
	if db == nil {
		logger.Error("failed to connect database", zap.Error(err))
		return
	}
	defer db.Close()
	for i := 0; i < 3; i++ {
		tenants, err := GetNewTenantsFromGCP(logger, date)
		if err != nil {
			logger.Error("failed, sleep 60s to retry", zap.Error(err))
			time.Sleep(time.Duration(60) * time.Second)
			continue
		}
		failed := 0
		for _, tenant := range tenants {
			if _, err = db.Exec("replace into tenant_meta.immutable_gcp_dwm_cloud_tenants (tenant_id, tenant_name, tenant_region, tenant_internal_user, tenant_created_at) values(?,?,?,?,?)",
				tenant.TenantId, tenant.TenantName, tenant.TenantRegion, tenant.TenantInternalUser, tenant.TenantCreatedAt); err != nil {
				failed++
				logger.Error("update tenant_meta.immutable_gcp_dwm_cloud_tenants failed", zap.Any("details", tenant), zap.Error(err))
				break
			}
		}
		if failed == 0 {
			break
		}
	}
}

func FatchServerlessActiveClusters() {
	date := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
	logger := otelzap.L().With(zap.String("trace_id", uuid.NewV1().String()), zap.String("date", date))
	logger.Info("FatchServerlessActiveClusters job start...")
	defer logger.Info("FatchServerlessActiveClusters job end...")

	db, err := getDB()
	if db == nil {
		logger.Error("failed to connect database", zap.Error(err))
		return
	}
	defer db.Close()
	logger.Info("get db client success")

	for i := 0; i < 3; i++ {
		failed := 0
		//for _, step := range []string{"1d", "7d", "30d"} {
		for _, step := range []string{"1d", "30d"} {
			res, err := GetServerlessActiveClusterFromMetrics(logger, date, step)
			if res == nil {
				failed++
				logger.Error("failed to get metrics, sleep 60s to retry", zap.Any(step, err))
				time.Sleep(time.Duration(60) * time.Second)
				continue
			}
			for key, count := range res {
				item := strings.Split(key, "/")
				if len(item) != 2 {
					failed++
					logger.Error("invalid key value", zap.Any(key, count))
					continue
				}
				provider := item[0]
				region := item[1]
				ServerlessDailyClusters.WithLabelValues(provider, region, step).Set(float64(count))
				if _, err = db.Exec("replace into serverless_tier.serverless_active_clusters(day, provider, region, step, cluster_num) values(?,?,?,?,?)",
					date, provider, region, step, count); err != nil {
					logger.Error("update serverless_tier.serverless_active_clusters failed", zap.Error(err))
				}
			}
		}
		if failed == 0 {
			break
		}
	}
}

func main() {
	logger := otelzap.L().With(zap.String("func", "main http server"), zap.String("trace_id", uuid.NewV1().String()))

	AwsRoleInit()

	c := cron.New()
	c.AddFunc("0 8 * * *", func() {
		go FatchServerlessDailyCost()
	})
	c.AddFunc("0 0 * * *", func() {
		go FatchCloudTenants()
	})
	c.AddFunc("0 1 * * *", func() {
		go FetchServerlessTenantAccessInfo()
	})
	c.AddFunc("0 2 * * *", func() {
		go FatchServerlessActiveClusters()
	})
	c.Start()

	go RunMetricsServer(logger)

	FatchServerlessDailyCost()
	FatchServerlessActiveClusters()

	r := gin.Default()
	r.GET("/:tag", func(c *gin.Context) {
		tag := c.Param("tag")
		if tag == "tenant" {
			FetchServerlessTenantAccessInfo()
		} else if tag == "cost" {
			FatchServerlessDailyCost()
		} else if tag == "tenants" {
			FatchCloudTenants()
		} else if tag == "active_clusters" {
			FatchServerlessActiveClusters()
		}
		c.String(200, "well done!")
	})
	r.GET("/s3/:bucket/:dir/:key", func(c *gin.Context) {
		bucket := c.Param("bucket")
		key := c.Param("key")
		dir := c.Param("dir")
		value := c.Param("value")
		if len(dir) > 0 {
			key = dir + "/" + key
		}
		logger.Info("s3://%s/%s value=%s", zap.Any(bucket, key), zap.Any("value", value))
		if err := S3NewObject(logger, bucket, key); err != nil {
			logger.Error("S3NewObject failed. s3://%s/%s, err=%v", zap.Any(bucket, key), zap.Error(err))
		}
		c.String(200, "well done")
	})
	r.Run(":8888")
}

/*
// rerun jobs
func main() {
	AwsRoleInit()
	updateServerlessDb(2)
}
*/
