package cron

import (
	"context"
	"errors"
	"fmt"
	"strings"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/server"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	templateversion "github.com/tidbcloud/cluster-service-ng/pkg/template/version"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	templateVersionUsageDetectorLockKey = "template-version-usage-detector"
	defaultTemplateUsageDetectorCron    = "0 */5 * * * *"
)

type TemplateVersionUsageDetectorJob struct {
	ngService          server.Service
	templateVersionSvc *templateversion.Service
	lockRepo           repo.LockRepo
	cronExpression     string
}

func NewTemplateVersionUsageDetectorJob(
	ngService server.Service,
	templateVersionSvc *templateversion.Service,
	lockRepo repo.LockRepo,
	cronExpression string,
) *TemplateVersionUsageDetectorJob {
	if strings.TrimSpace(cronExpression) == "" {
		cronExpression = defaultTemplateUsageDetectorCron
	}
	return &TemplateVersionUsageDetectorJob{
		ngService:          ngService,
		templateVersionSvc: templateVersionSvc,
		lockRepo:           lockRepo,
		cronExpression:     cronExpression,
	}
}

func (j *TemplateVersionUsageDetectorJob) Name() string {
	return "TemplateVersionUsageDetectorJob"
}

func (j *TemplateVersionUsageDetectorJob) CronExpression() string {
	return j.cronExpression
}

func (j *TemplateVersionUsageDetectorJob) Execute(ctx context.Context) error {
	unlockFunc, err := j.lock(ctx)
	if err != nil {
		tilog.Warn(ctx, "failed to acquire lock for template version usage detector, skip this round", zap.Error(err))
		return nil
	}
	defer func() { _ = unlockFunc() }()

	sharedPools, err := j.ngService.ListSharedPools(ctx, nil, nil)
	if err != nil {
		return fmt.Errorf("failed to list shared pools: %w", err)
	}

	unavailableCounters := map[templateUnavailableLabel]float64{}
	unknownCounters := map[templateUnknownLabel]float64{}

	for _, sharedPool := range sharedPools {
		if sharedPool.GetPhase() == apiv1.SharedPool_DELETING {
			continue
		}
		for _, comp := range sharedPool.GetComponentGroups() {
			templateName, err := entity.GetComponentTemplateName(comp)
			if err != nil || !templateversion.IsUsageDetectableComponentTemplate(templateName) {
				continue
			}
			componentType := comp.GetComponentType().String()
			currentVersion := comp.GetTemplateVersion()
			if currentVersion == "" {
				unknownCounters[templateUnknownLabel{
					templateName:  templateName,
					componentType: componentType,
				}]++
				continue
			}

			templateInfo, err := j.templateVersionSvc.LoadVersionInfo(ctx, templateName, currentVersion)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					unknownCounters[templateUnknownLabel{
						templateName:  templateName,
						componentType: componentType,
					}]++
					tilog.Warn(ctx, "component template version not found in registry",
						zap.String("sharedPoolId", sharedPool.GetId()),
						zap.String("componentId", comp.GetId()),
						zap.String("componentType", componentType),
						zap.String("templateName", templateName),
						zap.String("templateVersion", currentVersion),
					)
					continue
				}
				return fmt.Errorf("failed to load template version %s/%s: %w", templateName, currentVersion, err)
			}
			if templateInfo.GetStatus() == apiv1.TemplateVersionStatus_TEMPLATE_VERSION_STATUS_DISABLED ||
				templateInfo.GetStatus() == apiv1.TemplateVersionStatus_TEMPLATE_VERSION_STATUS_EXPIRED {
				unavailableCounters[templateUnavailableLabel{
					templateName:  templateName,
					status:        templateInfo.GetStatus().String(),
					componentType: componentType,
				}]++
			}
		}
	}

	updateTemplateUsageMetrics(unavailableCounters, unknownCounters)
	return nil
}

func (j *TemplateVersionUsageDetectorJob) lock(ctx context.Context) (func() error, error) {
	return j.lockRepo.Lock(ctx, &entity.Lock{
		Key:         templateVersionUsageDetectorLockKey,
		TimeoutSecs: 300,
	})
}

func updateTemplateUsageMetrics(
	unavailableCounters map[templateUnavailableLabel]float64,
	unknownCounters map[templateUnknownLabel]float64,
) {
	ensureMetricsRegistered()
	templateUsageMetricsMu.Lock()
	defer templateUsageMetricsMu.Unlock()

	currentUnavailable := make(map[string]struct{}, len(unavailableCounters))
	for label, count := range unavailableCounters {
		templateUnavailableUsageGauge.WithLabelValues(label.templateName, label.status, label.componentType).Set(count)
		key := label.key()
		currentUnavailable[key] = struct{}{}
		lastUnavailableLabels[key] = label
	}
	for key, label := range lastUnavailableLabels {
		if _, ok := currentUnavailable[key]; ok {
			continue
		}
		templateUnavailableUsageGauge.WithLabelValues(label.templateName, label.status, label.componentType).Set(0)
		delete(lastUnavailableLabels, key)
	}

	currentUnknown := make(map[string]struct{}, len(unknownCounters))
	for label, count := range unknownCounters {
		templateUnknownUsageGauge.WithLabelValues(label.templateName, label.componentType).Set(count)
		key := label.key()
		currentUnknown[key] = struct{}{}
		lastUnknownLabels[key] = label
	}
	for key, label := range lastUnknownLabels {
		if _, ok := currentUnknown[key]; ok {
			continue
		}
		templateUnknownUsageGauge.WithLabelValues(label.templateName, label.componentType).Set(0)
		delete(lastUnknownLabels, key)
	}
}
