package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/tidbcloud/metering/pkg/common/config"
	"github.com/tidbcloud/metering/pkg/common/constants"
	"github.com/tidbcloud/metering/pkg/common/models"
	"github.com/tidbcloud/metering/pkg/common/utils"
	"github.com/tidbcloud/metering/pkg/repoter"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var serviceconfigpath, dsn string
var checkts int64
var length int
var saveResult bool
var objperfix string

type ProjectMetering struct {
	TenantID              string    `gorm:"column:tenant_id"`
	ProjectID             string    `gorm:"column:project_id"`
	StartTs               time.Time `gorm:"column:start_ts"`
	ProjectMeteringStates string    `gorm:"column:project_metering_states"`
}
type ProjectMeteringStates struct {
	Detailes []ProjectMeteringStatesInner `json:"network_detail_states"`
}

type ProjectMeteringStatesInner struct {
	//{"cost": 48000000, "unit": "network-traffic:aws:ipv4", "usage": 9600000000}
	Cost  int64  `json:"cost"`
	Unit  string `json:"unit"`
	Usage int64  `json:"usage"`
}

type CompareResult struct {
	ProjectID  string
	TenantID   string
	SKU        string
	Diff       int64
	CompareTs  int64 `gorm:"autoCreateTime"`
	MeteringTs int64
	Comment    string
}

func main() {
	flag.StringVar(&serviceconfigpath, "serviceconfig", "./configs/serviceconfig.yaml", "service info.")
	flag.StringVar(&dsn, "dsn", "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local", "mysql dsn")
	flag.Int64Var(&checkts, "checkts", 0, "check timestamp")
	flag.IntVar(&length, "length", 0, "length")
	flag.StringVar(&objperfix, "objperfix", "dataplane-cost-storage/checkresult", "obj perfix")
	flag.BoolVar(&saveResult, "saveResult", false, "save result to obj")
	flag.Parse()

	serviceConfig, err := config.LoadServiceConfig(serviceconfigpath)
	if err != nil {
		log.Panicf("load service config failed. %s", err.Error())
	}
	ctx := context.TODO()
	logLevel := zapcore.DebugLevel
	err = logLevel.Set(serviceConfig.LogLevel)
	if err != nil {
		log.Panicf("set log level failed. %s", err.Error())
	}
	edsn := os.Getenv(dsn)
	if edsn == "" {
		edsn = dsn
	}
	logger, _ := zap.NewDevelopment()
	logger.Info("serviceconfig", zap.Any("serviceconfig", serviceConfig))
	loader, err := repoter.NewBillingRepoterLoad(&serviceConfig).Init()
	if err != nil {
		logger.Fatal("new loader failed", zap.Error(err))
	}
	db, err := gorm.Open(mysql.Open(edsn), &gorm.Config{})
	if err != nil {
		logger.Fatal("open mysql failed", zap.Error(err))
	}
	compaered := make([]*CompareResult, 0)
	if checkts == 0 {
		// get current UTC time
		now := time.Now().UTC()
		// get the start time of the previous day
		startOfPreviousDay := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, time.UTC)
		checkts = startOfPreviousDay.Unix()
	}
	startedts := checkts
	for i := 0; i < length; i++ {
		//build repoter path
		file := buildObjPath(&serviceConfig, checkts)
		//load repoter file
		rps, err := loader.LoadRepoterWithTs(ctx, file, 0)
		if err != nil {
			logger.Fatal("load repoter failed", zap.Error(err))
		}
		//load mysql data
		//2024-10-24
		var results []ProjectMetering
		sqltmpl := "SELECT tenant_id,project_id,start_ts,project_metering_states FROM project_meterings WHERE DATE(start_ts) = ?;"
		db.Raw(sqltmpl, time.Unix(checkts, 0).UTC().Format("2006-01-02")).Scan(&results)
		cmp := compare(logger, serviceConfig.Vendor, checkts, rps, results)
		compaered = append(compaered, cmp...)
		checkts = checkts - 86400
	}
	if saveResult {
		resultstore, err := NewCheckDBStore(&serviceConfig, objperfix).Init()
		if err != nil {
			logger.Fatal("new resultstore failed", zap.Error(err))
		}
		err = resultstore.Load(ctx, startedts)
		if err != nil {
			logger.Fatal("load db failed", zap.Error(err))
		}
		err = resultstore.Store(ctx, startedts, compaered)
		if err != nil {
			logger.Fatal("store db failed", zap.Error(err))
		}
		objpath, err := resultstore.Upload(ctx, startedts)
		if err != nil {
			logger.Fatal("upload db failed", zap.Error(err))
		}
		logger.Info("upload db success", zap.String("objpath", objpath))
	}
	logger.Info("compare result", zap.Any("compaered", compaered))
}

func buildObjPath(cfg *config.ServiceConfig, timestamp int64) string {
	var perfix string
	switch cfg.Vendor {
	case constants.VendorAWS:
		{
			perfix = cfg.TrafficMeteringConfig.Provider.AWS.StoreProvider.Perfix
		}
	case constants.VendorAzure:
		{
			perfix = cfg.TrafficMeteringConfig.Provider.Azure.StoreProvider.Perfix
		}
	case constants.VendorGCP:
		{
			perfix = cfg.TrafficMeteringConfig.Provider.GCP.StoreProvider.Perfix
		}
	}
	day := utils.GenerateDailyDate(timestamp)

	objpath := filepath.Join(perfix, fmt.Sprintf("%s/%s.db", day, day))
	return objpath
}

func compare(loger *zap.Logger, vdendor string, meteringTS int64, rps []*models.BillingReport, results []ProjectMetering) []*CompareResult {
	//compare
	var rpsmap = make(map[string]map[string]int64)
	for _, rp := range rps {
		//first key is tenantid+ project id
		labels, err := getjson(rp.Labels)
		if err != nil {
			log.Fatal("get json failed", zap.Error(err))
		}
		projectid := labels[constants.TagBillingProjectID]
		key := fmt.Sprintf("%d-%s", rp.TenantID, projectid)
		if _, ok := rpsmap[key]; !ok {
			rpsmap[key] = make(map[string]int64)
		}
		//innner key is old sku
		inner, ok := rpsmap[key][toOldSKU(rp.Unit)]
		if ok {
			rpsmap[key][toOldSKU(rp.Unit)] = inner + rp.Cost
		} else {
			rpsmap[key][toOldSKU(rp.Unit)] = rp.Cost
		}
	}
	//build result map
	resultmap := make(map[string]map[string]int64, 0)
	for _, result := range results {
		key := fmt.Sprintf("%s-%s", result.TenantID, result.ProjectID)
		//parse project_metering_states
		var states ProjectMeteringStates
		err := json.Unmarshal([]byte(result.ProjectMeteringStates), &states)
		if err != nil {
			loger.Fatal("unmarshal failed", zap.Error(err))
		}
		inner := make(map[string]int64, 0)
		for _, state := range states.Detailes {
			if strings.Contains(state.Unit, vdendor) {
				if in, ok := inner[toOldSKU(state.Unit)]; ok {
					inner[toOldSKU(state.Unit)] = in + state.Cost
				} else {
					inner[toOldSKU(state.Unit)] = state.Cost
				}
			}
		}
		if len(inner) > 0 {
			resultmap[key] = inner
		}
	}
	var compaereds = make([]*CompareResult, 0)
	if len(resultmap) == 0 {
		loger.Info("old metering is empty, may be not start metering")
		compaereds = append(compaereds, &CompareResult{
			ProjectID:  "",
			TenantID:   "",
			SKU:        "",
			Diff:       0,
			MeteringTs: meteringTS,
			Comment:    "old metering is empty, may be not start metering",
		})
		return compaereds
	}

	//compare
	for key, value := range resultmap {
		if rp, ok := rpsmap[key]; ok {
			for sku, cost := range value {
				info := strings.Split(key, "-")
				if rpCost, ok := rp[sku]; ok {
					if rpCost == cost {
						loger.Info("compare result eq", zap.String("key", key), zap.String("sku", sku), zap.Int64("cost", cost))
						compaereds = append(compaereds, &CompareResult{
							ProjectID:  info[1],
							TenantID:   info[0],
							SKU:        sku,
							Diff:       0,
							MeteringTs: meteringTS,
							Comment:    "compare result eq",
						})
					} else {
						//log compare result to s3 file and log at stdout
						loger.Info("compare result diff", zap.String("key", key), zap.String("sku", sku), zap.Int64("cost diff", rpCost-cost))
						compaereds = append(compaereds, &CompareResult{
							ProjectID:  info[1],
							TenantID:   info[0],
							SKU:        sku,
							Diff:       rpCost - cost,
							MeteringTs: meteringTS,
							Comment:    "compare result diff",
						})
					}
				} else {
					//log compare result to s3 file and log at stdout
					loger.Error("compare sku not found", zap.String("key", key), zap.String("sku", sku))
					cres := CompareResult{
						ProjectID:  info[1],
						TenantID:   info[0],
						SKU:        sku,
						Diff:       0 - cost,
						MeteringTs: meteringTS,
					}
					if cost == 0 {
						cres.Comment = "compare eq but new metering not keep"
					} else {
						cres.Comment = "compare sku not found"
					}
					compaereds = append(compaereds, &cres)
				}
			}
		} else {
			//log compare result to s3 file and log at stdout
			loger.Error("compare project and tennat not found", zap.String("key", key))
			info := strings.Split(key, "-")
			cres := CompareResult{
				ProjectID:  info[1],
				TenantID:   info[0],
				SKU:        "",
				Diff:       0,
				MeteringTs: meteringTS,
				Comment:    "compare project and tennat not found",
			}
			compaereds = append(compaereds, &cres)
		}
	}
	//log compare result to s3 file and log at stdout
	return compaereds
}

func toOldSKU(sku string) string {
	//parse sku like  network-traffic:aws:us-west-2:ipv4 to network-traffic:aws:ipv4
	skus := strings.Split(sku, ":")
	if len(skus) == 4 {
		return fmt.Sprintf("%s:%s:%s", skus[0], skus[1], skus[3])
	}
	return sku
}

func getjson(s string) (map[string]string, error) {
	var result map[string]string
	err := json.Unmarshal([]byte(s), &result)
	if err != nil {

		return nil, err
	}
	return result, nil
}
