package regionalservice

import (
	"context"
	"fmt"
	"strconv"

	"github.com/shopspring/decimal"

	"github.com/tidbcloud/observability/pkg/common/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"

	//metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/util/intstr"

	"github.com/pingcap/log"
	prometheusv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
	informers "github.com/prometheus-operator/prometheus-operator/pkg/client/informers/externalversions"
	"github.com/tidbcloud/observability/pkg/common/configs"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"github.com/tidbcloud/observability/pkg/resourcemanager"
	"go.uber.org/zap"
	"k8s.io/client-go/tools/cache"
)

type job struct {
	clusterID        string
	alertRules       prometheusv1.PrometheusRuleSpec
	jobType          constants.JobType
	clusterShareMode constants.ShareMode
	bizType          string
	isOverlayRule    bool
	ruleTemplateID   string
	cacheKey         string
}

type jobResponse struct {
	job
	err error
}

type workers struct {
	maxWorkers         int
	jobChannel         chan job
	jobResponseChannel chan jobResponse
}

var worker workers
var alertRuleSyncConfig structs.SyncAlertRuleConfig

func workerInit() {
	worker = workers{
		maxWorkers:         alertRuleSyncConfig.MaxWorkers,
		jobChannel:         make(chan job, alertRuleSyncConfig.MaxJobs),
		jobResponseChannel: make(chan jobResponse),
	}
	worker.start()
}

func getOverlayRule(clusterID string) (prometheusv1.PrometheusRuleSpec, error) {
	overlayRulesSpec := prometheusv1.PrometheusRuleSpec{
		Groups: make([]prometheusv1.RuleGroup, 0),
	}
	overlayRuleID := fmt.Sprintf(constants.OVERLAY_NAME_TEMPATE, clusterID)
	overlayRules, err := resourcemanager.GetPrometheusRulesReaderWriter(context.TODO(), false).GetPrometheusRule(context.TODO(), overlayRuleID)
	if err != nil {
		errs, ok := err.(errors.Error)
		if ok && errs.Code() != errors.PrometheusRuleNotFound || !ok {
			log.Error("get overlay rules failed", zap.Error(err))
			return overlayRulesSpec, err
		}
	}
	if overlayRules != nil {
		overlayRulesSpec = overlayRules.Spec
	}
	return overlayRulesSpec, nil
}

func getTemplateRule(templateName string) (prometheusv1.PrometheusRuleSpec, error) {
	templateRulesSpec := prometheusv1.PrometheusRuleSpec{
		Groups: make([]prometheusv1.RuleGroup, 0),
	}

	templateRules, err := resourcemanager.GetPrometheusRulesReaderWriter(context.TODO(), false).GetPrometheusRule(context.TODO(), templateName)
	if err != nil {
		return templateRulesSpec, err
	}
	if templateRules != nil {
		templateRulesSpec = templateRules.Spec
	}
	return templateRulesSpec, nil
}

func (w *workers) dealJob(j job) {
	defer func() {
		if r := recover(); r != nil {
			log.Error("sync alertrule workers panic", zap.Any("Panic", r))
			return
		}
	}()

	var err error
	defer func() {
		w.sendJobResponse(j, err)
	}()

	var overlayRulesSpec prometheusv1.PrometheusRuleSpec
	var templateRulSpec prometheusv1.PrometheusRuleSpec
	if j.isOverlayRule {
		templateRulSpec, err = getTemplateRule(j.ruleTemplateID)
		if err != nil {
			return
		}
		overlayRulesSpec = j.alertRules
	} else {
		overlayRulesSpec, err = getOverlayRule(j.clusterID)
		if err != nil {
			return
		}
		templateRulSpec = j.alertRules
	}

	switch j.jobType {
	case constants.SyncUpdate, constants.SyncPeriod:
		_, err = GetHandler().UpdateAlertRules(context.TODO(), message.UpdateAlertRulesReq{
			ID:               j.clusterID,
			MonitorPromRules: alertRuleMerge(templateRulSpec, overlayRulesSpec),
			Override:         true,
			BizType:          j.bizType,
			ShareMode:        j.clusterShareMode,
		})
		if err != nil {
			log.Error("UpdateAlertRules failed",
				zap.Error(err),
				zap.String("Cluster_ID", j.clusterID))
		}
		log.Debug("UpdateAlertRules job", zap.Any("Job", j))
		if j.jobType == constants.SyncUpdate && configs.GetServiceInfo().AlertRuleCacheConfig.Enabled {
			ruleQueryCache.delete(j.cacheKey)
		}

	case constants.SyncCreate:
		_, err = GetHandler().UpdateAlertRules(context.TODO(), message.UpdateAlertRulesReq{
			ID:               j.clusterID,
			MonitorPromRules: alertRuleMerge(templateRulSpec, overlayRulesSpec),
			Override:         true,
			BizType:          j.bizType,
			ShareMode:        j.clusterShareMode,
		})
		if err != nil {
			log.Error("createAlertRules failed",
				zap.Error(err),
				zap.String("Cluster_ID", j.clusterID))
		}
		log.Debug("createAlertRules job", zap.Any("Job", j))
	}
}

func (w *workers) start() {
	for i := 0; i < w.maxWorkers; i++ {
		go func() {
			for j := range w.jobChannel {
				w.dealJob(j)
			}
		}()
	}
}

func (w *workers) addJob(j job) {
	w.jobChannel <- j
}

func (w *workers) sendJobResponse(j job, e error) {
	w.jobResponseChannel <- jobResponse{
		err: e,
		job: j,
	}
}

func (w *workers) getJobResponse() jobResponse {
	return <-w.jobResponseChannel
}

func dispatch(ctx context.Context, promRule *prometheusv1.PrometheusRule, event constants.JobType) {
	continueToken := ""
	isRemanding := true
	jobs := make(map[string]job)
	retryTimes := make(map[string]int)
	log.Info("dispatch sync prometheusrule",
		zap.String("Prometheus_Rule_Name", promRule.Name),
		zap.Int("Event_Type", int(event)),
	)
	for isRemanding && promRule.Namespace == constants.AlertRuleTemplateNamespace {
		res, err := GetHandler().QueryClustersByRuleTemplateID(ctx, promRule.Name, message.ContinueRequest{
			Limit:         alertRuleSyncConfig.QueryClusterLimit,
			ContinueToken: continueToken,
		})
		if err != nil {
			log.Error("QueryClustersByRuleTemplateID failed",
				zap.Error(err),
				zap.String("Template_ID", promRule.Name))
			break
		}

		continueToken = res.ContinueResponse.ContinueToken

		if continueToken == "" {
			isRemanding = false
		}
		for _, cluster := range res.Clusters {
			if cluster.MuteAlert {
				continue
			}
			// ignore the rules for clusters using RDS.
			if conf, ok := configs.GetServiceInfo().BizTypeConfigs[cluster.BizType]; ok && conf.UseRDS {
				continue
			}
			jobs[cluster.ID] = job{
				clusterID:        cluster.ID,
				alertRules:       *promRule.Spec.DeepCopy(), // avoid promRule being modified by multiple goroutines
				jobType:          event,
				clusterShareMode: cluster.ShareMode,
				bizType:          cluster.BizType,
				isOverlayRule:    false,
				cacheKey: fmt.Sprintf(constants.RULE_CACHEKEY_TEMPLATE, cluster.TenantID,
					cluster.ProjectID, cluster.ApplicationID, cluster.BizType),
			}
			retryTimes[cluster.ID] = alertRuleSyncConfig.FailedRetryTimes
		}
	}

	if promRule.Namespace != constants.AlertRuleTemplateNamespace {
		clusterID := promRule.ObjectMeta.Labels[constants.LabelClusterID]
		shareMode := promRule.ObjectMeta.Labels[constants.LabelShareMode]
		bizType := promRule.ObjectMeta.Labels[constants.LabelBizType]
		ruleTemplateID := promRule.ObjectMeta.Labels[constants.LabelTemplateID]
		tenantID := promRule.ObjectMeta.Labels[constants.LabelTenantID]
		projectID := promRule.ObjectMeta.Labels[constants.LabelProjectID]
		applicationID := promRule.ObjectMeta.Labels[constants.LabelApplicationID]
		jobs[clusterID] = job{
			clusterID:        strings.TrimPrefix(clusterID, "overlay-"),
			alertRules:       *promRule.Spec.DeepCopy(),
			jobType:          event,
			clusterShareMode: constants.ShareMode(shareMode),
			bizType:          bizType,
			isOverlayRule:    true,
			ruleTemplateID:   ruleTemplateID,
			cacheKey:         fmt.Sprintf(constants.RULE_CACHEKEY_TEMPLATE, tenantID, projectID, applicationID, bizType),
		}
		retryTimes[clusterID] = alertRuleSyncConfig.FailedRetryTimes
	}

	log.Info("dispatch sync prometheusrule",
		zap.Int("Job_Number", len(jobs)))

	go func(job2 map[string]job) {
		for _, j := range jobs {
			worker.addJob(j)
		}
	}(jobs)

	completed := len(jobs)
	for completed > 0 {
		response := worker.getJobResponse()
		if response.err != nil && retryTimes[response.clusterID] > 0 {
			log.Error("requeue job",
				zap.Error(response.err),
				zap.String("Pro_Rule_Name", promRule.Name),
				zap.String("Cluster_ID", response.clusterID),
			)
			// start a new goroutine here to avoid deadlock
			// for more details refer to https://github.com/tidbcloud/observability/issues/850
			// TODO: refactor job channel implementation to reconciler workqueue.
			go worker.addJob(response.job)
			retryTimes[response.clusterID]--
			continue
		}
		completed--
	}
}

// Using NewSharedInformerFactoryWithOptions constructs a new informer for PrometheusRule
// to sync prometheusrule templates to  clusters, dependency prometheus-operator
func StartSync(ctx context.Context, syncConfig structs.SyncAlertRuleConfig, serviceInfo structs.ServiceInfo) {
	configs.Init(serviceInfo)
	alertRuleSyncConfig = syncConfig
	workerInit()
	stop := make(chan struct{})
	syncPerod := make(map[string]time.Time)
	clientset := resourcemanager.GetPrometheusOperatorClient()
	opt := informers.WithTweakListOptions(func(opts *metav1.ListOptions) {
		opts.LabelSelector = labels.SelectorFromSet(labels.Set{constants.ExternalLabelOverlay: "true"}).String()
	})
	fa := informers.NewSharedInformerFactoryWithOptions(clientset, alertRuleSyncConfig.ResyncPeriod, opt)
	prometheusRuleInformer := fa.Monitoring().V1().PrometheusRules()
	index := prometheusRuleInformer.Informer()
	// nolint:errcheck
	index.AddEventHandlerWithResyncPeriod(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			promRule, _ := obj.(*prometheusv1.PrometheusRule)
			if promRule == nil {
				return
			}
			// ignore the rules for clusters using RDS.
			bizType := promRule.Labels[constants.LabelBizType]
			if conf, ok := configs.GetServiceInfo().BizTypeConfigs[bizType]; ok && conf.UseRDS {
				return
			}
			dispatch(ctx, promRule.DeepCopy(), constants.SyncCreate)
		},
		UpdateFunc: func(oldObj, newObj interface{}) {
			promRule, _ := newObj.(*prometheusv1.PrometheusRule)
			oldPromeRule, _ := oldObj.(*prometheusv1.PrometheusRule)
			if promRule == nil || oldPromeRule == nil {
				return
			}
			// ignore the rules for clusters using RDS.
			bizType := promRule.Labels[constants.LabelBizType]
			if conf, ok := configs.GetServiceInfo().BizTypeConfigs[bizType]; ok && conf.UseRDS {
				return
			}
			var timeduration time.Duration
			if perod, ok := syncPerod[promRule.Name]; !ok {
				syncPerod[promRule.Name] = time.Now()
			} else {
				lastTime := perod
				timeduration = time.Since(lastTime)
			}
			if promRule.Generation != oldPromeRule.Generation || timeduration.Seconds() > alertRuleSyncConfig.RotationInterval.Seconds() {
				jobType := constants.SyncPeriod
				if promRule.Generation != oldPromeRule.Generation {
					jobType = constants.SyncUpdate
				}
				dispatch(ctx, promRule.DeepCopy(), jobType)
				syncPerod[promRule.Name] = time.Now()
			}
		},
	}, alertRuleSyncConfig.ResyncPeriod)
	start(fa, stop)
}

var start = func(fa informers.SharedInformerFactory, stop chan struct{}) {
	fa.Start(stop)
}

func alertRuleMerge(templateRules, overlayRules prometheusv1.PrometheusRuleSpec) prometheusv1.PrometheusRuleSpec {
	promSpec := prometheusv1.PrometheusRuleSpec{}
	promSpec.Groups = make([]prometheusv1.RuleGroup, 0)

	overlayRulesInfo := make(map[string]prometheusv1.Rule)

	if overlayRules.Groups != nil {
		for _, ruelGroup := range overlayRules.Groups {
			if ruelGroup.Rules == nil {
				continue
			}
			for _, rule := range ruelGroup.Rules {
				ruleID := fmt.Sprintf(constants.RULEID_TEMPLATE, ruelGroup.Name,
					rule.Labels[constants.RuleLabelID])
				overlayRulesInfo[ruleID] = rule
			}
		}
	}

	for _, g := range templateRules.Groups {
		externalRules := make([]prometheusv1.Rule, 0)
		if g.Rules == nil {
			continue
		}
		for _, rule := range g.Rules {
			ruleID := fmt.Sprintf(constants.RULEID_TEMPLATE, g.Name,
				rule.Labels[constants.RuleLabelID])
			if overlayRule, ok := overlayRulesInfo[ruleID]; ok {
				modifiedTag, _ := strconv.Atoi(overlayRule.Labels[constants.RuleLabelUserModified])
				if modifiedTag > 0 {
					if modifiedTag&constants.RuleStatusModifiedMask > 0 {
						rule.Labels[constants.RuleLabelStatus] = overlayRule.Labels[constants.RuleLabelStatus]
					}
					if modifiedTag&constants.RuleThresholdModifiedMask > 0 {
						ruleExpr := structs.GenerateOverLayAlertRuleEXPR(rule.Expr.String())
						overlayRuleExpr := structs.GenerateOverLayAlertRuleEXPR(overlayRule.Expr.String())
						decimalVal := decimal.NewFromFloat(overlayRuleExpr.Value)
						rule.Expr = intstr.FromString(ruleExpr.Expr + ruleExpr.Operator + decimalVal.String())
					}
					if modifiedTag&constants.RuleForModifiedMask > 0 {
						rule.For = overlayRule.For
					}
				}
			}
			if rule.Labels[constants.RuleLabelStatus] == string(constants.RULE_DISABLED) {
				continue
			}
			if rule.Labels[constants.RuleLabelVisibility] == "external" {
				rule = replaceExternalRulePlaceHolder(rule)
				//delete(rule.Labels, constants.RuleLabelID)
				delete(rule.Labels, constants.RuleLabelCanEdit)
				delete(rule.Labels, constants.RuleLabelOperator)
				delete(rule.Labels, constants.RuleLabelMonitortarget)
				delete(rule.Labels, constants.RuleLabelStatus)
			}
			externalRules = append(externalRules, rule)
		}
		group := prometheusv1.RuleGroup{
			Name:     g.Name,
			Interval: g.Interval,
			Rules:    externalRules,
		}
		promSpec.Groups = append(promSpec.Groups, group)
	}
	return promSpec

}
