package instancemetering

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strconv"
	"time"

	"github.com/tidbcloud/metering/pkg/errors"

	log "github.com/sirupsen/logrus"

	"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/facade"
)

var cachedQueryClusterMetaResult QueryClusterMetaResult

type QueryClusterMetaResult struct {
	QueryTime int64
	Data      []models.ClusterMeta
}

func ExecuteClusterMetaTask(ctx context.Context, timestamp int64) error {
	var data = cachedQueryClusterMetaResult.Data
	var err error
	if cachedQueryClusterMetaResult.QueryTime == 0 || timestamp > cachedQueryClusterMetaResult.QueryTime {
		data = []models.ClusterMeta{}
		clusters, err := facade.GetTiDBClusterMetaQuerier(ctx).List(ctx, timestamp)
		if err != nil {
			return errors.Wrap(errors.GrpcServiceError, "TiDBClusterMetaQuerier.List(ctx, timestamp) failed", err)
		}
		for _, cluster := range clusters {
			if cluster.IsScaling {
				clusterID := cluster.ClusterID
				var err error
				uClusterID, err := strconv.ParseUint(clusterID, 10, 64)
				if err != nil {
					return errors.Wrap(errors.ParamParseError, fmt.Sprintf("strconv.ParseUint(%s, 10, 64)", clusterID), err)
				}
				components, err := facade.GetTiDBClusterMetaQuerier(ctx).Detail(ctx, timestamp, uClusterID)
				if err != nil {
					return errors.Wrap(errors.GrpcServiceError, "TiDBClusterMetaQuerier.Detail(ctx, timestamp) failed", err)
				}
				originComponentJson, err := json.Marshal(cluster.Components)
				if err != nil {
					return errors.Wrap(errors.MarshalError, "Marshal origin cluster.Components failed", err)
				}
				newComponentJson, err := json.Marshal(components)
				if err != nil {
					return errors.Wrap(errors.MarshalError, "Marshal new Components failed", err)
				}
				log.Infof("replace components info because of scaling, originalComponentInfo %s, new ComponentInfo %s", originComponentJson, newComponentJson)

				cluster.Components = components
			}
			data = append(data, *cluster)
		}

		newTime := time.Now().Unix()

		cachedQueryClusterMetaResult = QueryClusterMetaResult{
			QueryTime: newTime,
			Data:      data,
		}
	} else {
		log.Warnf("get data from cachedQueryClusterMetaResult, data cached at %d, get by %d", cachedQueryClusterMetaResult.QueryTime, timestamp)
		//update the timestamp for each query
		for i := 0; i < len(data); i++ {
			data[i].Timestamp = timestamp
		}
	}

	dir := fmt.Sprintf("%s/%s/%s", "data", constants.S3DataPrefix, "meta")
	err = os.MkdirAll(dir, 0755)
	if err != nil {
		return errors.Wrap(errors.OSError, fmt.Sprintf("os.MkdirAll(%s, 0755) failed", dir), err)
	}
	localFilePath := fmt.Sprintf("%s/%d.%s", dir, timestamp, "sqlite")
	// Remove the file if it exists, ignore error if file doesn't exist
	_ = os.Remove(localFilePath)
	db, err := GetClusterMetaDB(localFilePath)
	if err != nil {
		return err
	}
	defer db.Close(ctx)

	if err := db.SaveClusterMeta(ctx, data); err != nil {
		return err
	}

	return nil
}

func EnsureMeta(ctx context.Context, timestamp int64) (string, error) {
	clusterMetaFile := fmt.Sprintf("%s/%d.%s", fmt.Sprintf("%s/%s/%s", constants.LocalDataRoot, constants.S3DataPrefix, constants.ClusterMetaTaskName), timestamp, "sqlite")

	_, err := os.Stat(clusterMetaFile)
	if err == nil {
		return clusterMetaFile, nil
	}
	if os.IsNotExist(err) {
		_, err = facade.GetScheduleTaskDataSync(ctx).Download(ctx, constants.ClusterMetaTaskName, timestamp)
		if err != nil {
			log.Errorf("ScheduleTaskDataSync.Download(ctx, %s, %d) failed, %s", constants.ClusterMetaTaskName, timestamp, err.Error())
			return "", errors.Wrap(errors.S3Error, fmt.Sprintf("ScheduleTaskDataSync.Download(ctx, %s, %d)", constants.ClusterMetaTaskName, timestamp), err)
		}
	}

	return clusterMetaFile, nil
}

func EnsureBilling(ctx context.Context, timestamp int64) (string, error) {
	billingFile := fmt.Sprintf("%s/%d.%s", fmt.Sprintf("%s/%s/%s", constants.LocalDataRoot, constants.S3DataPrefix, constants.BillingTaskName), timestamp, "sqlite")

	_, err := os.Stat(billingFile)
	if err == nil {
		return billingFile, nil
	}
	if os.IsNotExist(err) {
		_, err = facade.GetScheduleTaskDataSync(ctx).Download(ctx, constants.BillingTaskName, timestamp)
		if err != nil {
			log.Errorf("ScheduleTaskDataSync.Download(ctx, %s, %d) failed, %s", constants.BillingTaskName, timestamp, err.Error())
			return "", errors.Wrap(errors.S3Error, fmt.Sprintf("ScheduleTaskDataSync.Download(ctx, %s, %d)", constants.BillingTaskName, timestamp), err)
		}
	}

	return billingFile, nil
}

func ExecuteBillingDataTask(ctx context.Context, timestamp int64) error {
	var err error

	clusterMetaFile, err := EnsureMeta(ctx, timestamp)
	if err != nil {
		return err
	}

	clusterMetaDB, err := GetClusterMetaDB(clusterMetaFile)
	if err != nil {
		return err
	}
	defer clusterMetaDB.Close(ctx)

	metas, err := clusterMetaDB.ListClusterMeta(ctx)
	if err != nil {
		return err
	}

	var reports []*models.BillingReport
	var reportsV2 []*models.BillingReportV2

	for _, meta := range metas {
		report, err := meta.GenerateBillingReport()
		if err != nil {
			return err
		}
		reportV2, err := meta.GenerateBillingReportV2()
		if err != nil {
			return err
		}
		reports = append(reports, report...)
		reportsV2 = append(reportsV2, reportV2...)
	}

	dir := fmt.Sprintf("%s/%s/%s", constants.LocalDataRoot, constants.S3DataPrefix, constants.BillingTaskName)
	err = os.MkdirAll(dir, 0755)
	if err != nil {
		return err
	}
	billingReportFile := fmt.Sprintf("%s/%d.%s", dir, timestamp, "sqlite")
	// Remove the file if it exists, ignore error if file doesn't exist
	_ = os.Remove(billingReportFile)
	billingReportDB, err := GetBillingReportDB(billingReportFile)
	if err != nil {
		return err
	}
	defer billingReportDB.Close(ctx)

	err = billingReportDB.SaveBillingReports(ctx, reports)
	if err != nil {
		return err
	}
	err = billingReportDB.SaveBillingReportsV2(ctx, reportsV2)
	if err != nil {
		return err
	}
	return nil
}

func ExecuteBillingReportTask(ctx context.Context, timestamp int64) error {
	billingFile, err := EnsureBilling(ctx, timestamp)
	if err != nil {
		return err
	}

	billingDB, err := GetBillingReportDB(billingFile)
	if err != nil {
		return err
	}
	defer billingDB.Close(ctx)

	reports, err := billingDB.ListBillingReports(ctx)
	if err != nil {
		return err
	}
	reportsV2, err := billingDB.ListBillingReportsV2(ctx)
	if err != nil {
		return err
	}

	toReport, err := ChooseBillingReporterVersion(timestamp, reports, reportsV2)
	if err != nil {
		return err
	}

	for _, report := range toReport {
		var needsend *models.BillingReport
		switch report := report.(type) {
		case *models.BillingReport:
			needsend = report
			log.Debugf("report is BillingReport %v", needsend)
		case *models.BillingReportV2:
			r, err := report.Convert2OldReporter()
			if err != nil {
				log.Errorf("Convert2OldReporter failed, %s", err.Error())
				return errors.Wrap(errors.ConvertError, "Convert2OldReporter failed", err)
			}
			//we should skip all cost is 0 report, additional io may be 0
			if r.Cost == 0 {
				log.Infof("skip cost is 0 report,report unit is %s id is : %s", r.Unit, r.CostID)
				continue
			}
			needsend = &r
			log.Debugf("report is BillingReportV2 %v", needsend)
		}
		e := facade.GetMeasureSender(ctx).SendMeasure(ctx, *needsend)
		if e != nil {
			log.Errorf("SendMeasure failed, %s", e.Error())
			return errors.Wrap(errors.SendMessureError, "SendMeasure failed", err)
		}
	}

	dir := fmt.Sprintf("%s/%s/%s", constants.LocalDataRoot, constants.S3DataPrefix, constants.ReportTaskName)
	err = os.MkdirAll(dir, 0755)
	if err != nil {
		return errors.Wrap(errors.OSError, fmt.Sprintf("os.MkdirAll(%s, 0755) failed", dir), err)
	}
	reportFile := fmt.Sprintf("%s/%d.%s", dir, timestamp, "sqlite")
	reader, err := os.Open(billingFile)
	if err != nil {
		return errors.Wrap(errors.OSError, fmt.Sprintf("os.Open(%s) failed", reportFile), err)
	}
	writer, err := os.Create(reportFile)
	if err != nil {
		return errors.Wrap(errors.OSError, fmt.Sprintf("os.Create(%s) failed", reportFile), err)
	}
	_, err = io.Copy(writer, reader)
	if err != nil {
		return errors.Wrap(errors.IOError, "io.Copy() failed", err)
	}

	return nil
}

func ChooseBillingReporterVersion(timestamp int64, reports []*models.BillingReport, reportsV2 []*models.BillingReportV2) ([]interface{}, error) {
	var result = make([]interface{}, 0)
	f := config.GetConfig().InstanceMeteringConfig.GetFeatureGateFilter(constants.FeatureGateNewSKU)
	disableNewSkuFilter := config.GetConfig().InstanceMeteringConfig.GetFeatureGateFilter(constants.FeatureGateNewSKUDisabled)
	doubleFilter := config.GetConfig().InstanceMeteringConfig.GetFeatureGateFilter(constants.FeatureGateNewSKUDouble)
	disableDoubleFilter := config.GetConfig().InstanceMeteringConfig.GetFeatureGateFilter(constants.FeatureGateNewSKUDoubleDisabled)
	tiproxyFilter := config.GetConfig().InstanceMeteringConfig.GetFeatureGateFilter(constants.FeatureGateTiproxyMetering)
	for _, r := range reports {
		// skip aws and gcp before switch
		skip, err := skipRepoter(timestamp, r)
		if err != nil {
			return nil, err
		}
		if skip {
			continue
		}
		tenantid := strconv.FormatUint(r.TenantID, 10)
		if r.IsTiproxySKU() {
			if !tiproxyFilter.IsTenantSwitch(tenantid, timestamp) {
				log.Debugf("tenantid %s not switch tiproxy, skip!, timestamp is %d", tenantid, timestamp)
				continue
			}
		}
		// if tenant in disable new sku list, use old sku
		if disableNewSkuFilter.IsTenantSwitch(tenantid, timestamp) {
			log.Debugf("tenantid %s in disable new sku list, use old sku, timestamp is %d", tenantid, timestamp)
			result = append(result, r)
			continue
		}
		if f.IsTenantSwitch(tenantid, timestamp) {
			// if tenant switch, use new sku
			if (!disableDoubleFilter.IsTenantSwitch(tenantid, timestamp)) && doubleFilter.IsTenantSwitch(tenantid, timestamp) {
				if r.IsNodeSKU() || r.IsTiproxySKU() {
					continue
				}
				log.Debugf("tenant switch to new sku, but not node sku, double write %s", r.Unit)
			} else {
				continue
			}
		}
		// add to list to report
		result = append(result, r)
	}
	for _, r := range reportsV2 {
		// skip aws and gcp before switch
		skip, err := skipRepoterV2(timestamp, r)
		if err != nil {
			return nil, err
		}
		if skip {
			continue
		}
		tenantid := strconv.FormatUint(r.TenantID, 10)
		// if tenant in disable new sku list, use old sku
		if disableNewSkuFilter.IsTenantSwitch(tenantid, timestamp) {
			log.Debugf("tenantid %s in disable new sku list, use old sku, skip any new sku", tenantid)
			continue
		}
		if r.IsTiproxySKU() {
			if !tiproxyFilter.IsTenantSwitch(tenantid, timestamp) {
				log.Debugf("tenantid %s not switch tiproxy, skip!, timestamp is %d", tenantid, timestamp)
				continue
			}
		}
		if !f.IsTenantSwitch(tenantid, timestamp) {
			// if tenant not  switch, use old sku
			log.Debugf("tenantid not switch, use old sku, tenantid is %s, timestamp is %d", tenantid, timestamp)
			continue
		}
		// add to list to report
		result = append(result, r)
	}
	return result, nil
}

func getjson(s string) (map[string]string, error) {
	var result map[string]string
	err := json.Unmarshal([]byte(s), &result)
	if err != nil {
		return nil, errors.Wrap(errors.UnmarshalError, "json.Unmarshal result failed", err)
	}
	return result, nil
}

func skipRepoter(timestamp int64, repoter *models.BillingReport) (bool, error) {
	cfg := config.GetConfig()
	//we should skip all cost is 0 report
	if repoter.Cost == 0 {
		log.Infof("skip cost is 0 report,report id is : %s", repoter.CostID)
		return true, nil
	}
	if timestamp < cfg.InstanceMeteringConfig.BillingReportSwitchTime {
		j, err := getjson(repoter.Labels)
		if err != nil {
			return false, errors.Wrap(errors.MarshalError, "getjson for repoter.Labels failed", err)
		}
		if j["provider"] != "azure" {
			tenantID := strconv.FormatUint(repoter.TenantID, 10)
			for _, v := range cfg.InstanceMeteringConfig.TenantWithSwitchTimes {
				if v.TenantId == tenantID && timestamp >= v.SwitchTime {
					log.Infof("send repoter to billing because tenantid %s in whitelist and time %d catched", tenantID, timestamp)
					return false, nil
				}
			}
			return true, nil
		}
	}
	j, err := getjson(repoter.Labels)
	if err != nil {
		return false, errors.Wrap(errors.MarshalError, "getjson for repoter.Labels failed", err)
	}
	if j["provider"] == "azure" {
		// if provider is azure, allow all report
		return false, nil
	}
	// if tenantid in block list, we should skip when timestamp < block list time
	tenantID := strconv.FormatUint(repoter.TenantID, 10)
	for _, v := range cfg.InstanceMeteringConfig.TenantBlockListFullSwitchTime {
		if v.TenantId == tenantID && timestamp < v.SwitchTime {
			log.Infof("tenantid %s in block list, skip report, timestamp is %d", tenantID, timestamp)
			return true, nil
		}
	}
	return false, nil
}

func skipRepoterV2(timestamp int64, repoter *models.BillingReportV2) (bool, error) {
	cfg := config.GetConfig()
	//we should skip all cost is 0 report
	if repoter.Cost == 0 {
		log.Infof("skip cost is 0 reportV2,reportV2 id is : %s", repoter.CostID)
		return true, nil
	}
	if timestamp < cfg.InstanceMeteringConfig.BillingReportSwitchTime {
		j, err := getjson(repoter.Labels)
		if err != nil {
			return false, errors.Wrap(errors.MarshalError, "getjson for repoter.Labels failed", err)
		}
		if j["provider"] != "azure" {
			tenantID := strconv.FormatUint(repoter.TenantID, 10)
			for _, v := range cfg.InstanceMeteringConfig.TenantWithSwitchTimes {
				if v.TenantId == tenantID && timestamp >= v.SwitchTime {
					log.Infof("send repoterV2 to billing because tenantid %s in whitelist and time %d catched", tenantID, timestamp)
					return false, nil
				}
			}
			return true, nil
		}
	}
	j, err := getjson(repoter.Labels)
	if err != nil {
		return false, errors.Wrap(errors.MarshalError, "getjson for repoter.Labels failed", err)
	}
	if j["provider"] == "azure" {
		// if provider is azure, allow all report
		return false, nil
	}
	// if tenantid in block list, we should skip when timestamp < block list time
	tenantID := strconv.FormatUint(repoter.TenantID, 10)
	for _, v := range cfg.InstanceMeteringConfig.TenantBlockListFullSwitchTime {
		if v.TenantId == tenantID && timestamp < v.SwitchTime {
			log.Infof("tenantid %s in block list, skip report, timestamp is %d", tenantID, timestamp)
			return true, nil
		}
	}
	return false, nil
}
