package schedule

import (
	"context"
	"strconv"
	"time"

	"github.com/tidbcloud/metering/pkg/common/config"
	"github.com/tidbcloud/metering/pkg/common/constants"
	"github.com/tidbcloud/metering/pkg/common/metrics"
	"github.com/tidbcloud/metering/pkg/networkmetering"
	"github.com/tidbcloud/metering/pkg/repoter"
	"go.uber.org/zap"
)

var getMetering = GetMetering

// NetworkTask is a task for network metering
// it will run the metering and report
type NetworkTask struct {
	conf      *config.ServiceConfig
	successed map[int64]interface{}
	subsucced map[int64]map[int64]interface{}
	log       *zap.Logger
}

func NewNetWorkTask(conf *config.ServiceConfig, log *zap.Logger) (*NetworkTask, error) {

	return &NetworkTask{
		conf:      conf,
		successed: make(map[int64]interface{}),
		subsucced: make(map[int64]map[int64]interface{}),
		log:       log,
	}, nil
}

func (n *NetworkTask) Run(ctx context.Context) error {

	// 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)
	ts := startOfPreviousDay.Unix()
	if _, ok := n.successed[ts]; ok {
		n.log.Info("task has been successed skip metering", zap.Int64("timestamp", ts))
		return nil
	}
	metering, repoter, err := getMetering(n.conf, n.log)
	if err != nil {
		n.log.Error("get metering work failed", zap.Error(err))
		labels := map[string]string{
			constants.MetricsMeterTs:         strconv.FormatInt(ts, 10),
			constants.MetricsFailedType:      "metering",
			constants.MetricsLblProviderType: n.conf.Vendor,
		}
		metrics.MetricsNetworkFailedCount.With(labels).Inc()
		return err
	}
	n.log.Info("start metering", zap.Int64("timestamp", ts))
	beforemetering := time.Now().Unix()
	objectpath, getted, err := metering.Metering(ctx, ts)
	aftermetering := time.Now().Unix()

	metrics.MetricsNetworkMeterDuration.With(map[string]string{
		constants.MetricsLblProviderType:  n.conf.Vendor,
		constants.MetricsMeterTs:          strconv.FormatInt(ts, 10),
		constants.MetricsNetworkMeterType: "meter",
	}).Set(float64(aftermetering - beforemetering))
	if err != nil {
		n.log.Error("metering failed", zap.Int64("timestamp", ts), zap.Error(err))
		labels := map[string]string{
			constants.MetricsMeterTs:         strconv.FormatInt(ts, 10),
			constants.MetricsFailedType:      "metering",
			constants.MetricsLblProviderType: n.conf.Vendor,
		}
		metrics.MetricsNetworkFailedCount.With(labels).Inc()
		return err
	}
	if !getted {
		n.log.Info("no data to metering,we should metering later", zap.Int64("timestamp", ts))
		return n.ReRunPreDay(ctx, metering, repoter, ts)
	}
	err = repoter.Report(ctx, objectpath)
	if err != nil {
		n.log.Error("report failed", zap.Int64("timestamp", ts), zap.Error(err))
		labels := map[string]string{
			constants.MetricsMeterTs:         strconv.FormatInt(ts, 10),
			constants.MetricsFailedType:      "report",
			constants.MetricsLblProviderType: n.conf.Vendor,
		}
		metrics.MetricsNetworkFailedCount.With(labels).Inc()
		return err
	}
	err = n.ReRunPreDay(ctx, metering, repoter, ts)
	if err != nil {
		n.log.Error("rerun pre day metering failed", zap.Int64("timestamp", ts), zap.Error(err))
		return err
	}
	// save checkpoint
	n.log.Info("task successed", zap.Int64("run's days:", ts))
	n.successed[ts] = true

	return nil
}

// rerun pre day metering
func (n *NetworkTask) ReRunPreDay(ctx context.Context, metering networkmetering.Metering, repoter repoter.Repoter, ts int64) error {
	if _, ok := n.subsucced[ts]; !ok {
		n.subsucced[ts] = make(map[int64]interface{})
	}
	oldts := ts
	for i := 1; i <= n.conf.TrafficMeteringConfig.RerunDays; i++ {
		// move to the previous day
		ts -= 24 * 60 * 60
		if _, ok := n.subsucced[oldts][ts]; ok {
			n.log.Info("rerun task has been successed skip rerun metering", zap.Int64("timestamp", ts))
			continue
		}
		// add tsdiff to ctx
		ctx := context.WithValue(ctx, constants.CtxKeyRerunTsDiff, oldts-ts)
		n.log.Info("start rerun metering", zap.Int64("timestamp", ts))
		beforemetering := time.Now().Unix()
		objectpath, getted, err := metering.Metering(ctx, ts)
		aftermetering := time.Now().Unix()
		metrics.MetricsNetworkMeterDuration.With(map[string]string{
			constants.MetricsLblProviderType:  n.conf.Vendor,
			constants.MetricsMeterTs:          strconv.FormatInt(ts, 10),
			constants.MetricsNetworkMeterType: "rerun",
		}).Set(float64(aftermetering - beforemetering))
		if err != nil {
			n.log.Error("rerun metering failed", zap.Int64("timestamp", ts), zap.Error(err))
			labels := map[string]string{
				constants.MetricsMeterTs:         strconv.FormatInt(ts, 10),
				constants.MetricsFailedType:      "metering",
				constants.MetricsLblProviderType: n.conf.Vendor,
			}
			metrics.MetricsNetworkFailedCount.With(labels).Inc()
			return err
		}
		if !getted {
			n.log.Info("no data to rerun metering,we should metering next day", zap.Int64("timestamp", ts))
			n.subsucced[oldts][ts] = true
			continue
		}
		err = repoter.Report(ctx, objectpath)
		if err != nil {
			n.log.Error("report failed", zap.Int64("timestamp", ts), zap.Error(err))
			labels := map[string]string{
				constants.MetricsMeterTs:         strconv.FormatInt(ts, 10),
				constants.MetricsFailedType:      "report",
				constants.MetricsLblProviderType: n.conf.Vendor,
			}
			metrics.MetricsNetworkFailedCount.With(labels).Inc()
			return err
		}
		// save checkpoint
		n.subsucced[oldts][ts] = true

	}
	n.log.Info("rerun task successed", zap.Int64("earliest days", ts))
	return nil
}

func GetMetering(conf *config.ServiceConfig, log *zap.Logger) (networkmetering.Metering, repoter.Repoter, error) {
	metering, err := networkmetering.NewNetworkMetering(conf, log.With(zap.String("module", "networkmetering")))
	if err != nil {
		return nil, nil, err
	}
	repoter, err := repoter.NewBillingRepoter(conf, log.With(zap.String("module", "repoter")))
	if err != nil {
		return nil, nil, err
	}
	return metering, repoter, nil
}
