package billing_svc

import (
	"context"
	"errors"
	"fmt"
	"strconv"

	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/domain/repo"
	"github.com/tidbcloud/serverless-service/internal/domain/vo"
	"github.com/tidbcloud/serverless-service/internal/metric"
	"github.com/tidbcloud/serverless-service/internal/utils"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
	api_errors "k8s.io/apimachinery/pkg/api/errors"
)

var (
	Tracer           = otel.Tracer("metering")
	ExitReasonNormal = "normal"
)

type MeteringService struct {
	billingFacade facade.BillingFacade
	infraFacade   facade.InfraFacade
	o11yFacade    facade.O11yFacade
	featureGate   facade.FeatureGate
	exportRepo    repo.ExportRepo
	mgmtFacade    facade.MgmtSvcFacade
}

func NewMeteringService(
	billingFacade facade.BillingFacade,
	infraFacade facade.InfraFacade,
	o11yFacade facade.O11yFacade,
	featureGate facade.FeatureGate,
	exportRepo repo.ExportRepo,
	mgmtFacade facade.MgmtSvcFacade,
) *MeteringService {
	return &MeteringService{
		billingFacade: billingFacade,
		infraFacade:   infraFacade,
		o11yFacade:    o11yFacade,
		featureGate:   featureGate,
		exportRepo:    exportRepo,
		mgmtFacade:    mgmtFacade,
	}
}

func updateExportMeterJobMetric(exportId, exitReason string, err error) {
	status := metric.StatusOK
	if err != nil {
		status = metric.StatusErr
	}
	metric.ExportMeterJobProcessCounter.WithLabelValues(exportId, status, exitReason).Inc()
}

// ExecuteExport metering ru generated by the export
func (s *MeteringService) ExecuteExport(ctx context.Context) (err error) {
	ctx, span := Tracer.Start(ctx, "metering.ExecuteExport")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	log := utils.GetWorkerLoggerFromContext(ctx).With(zap.String("metering_type", "export"))
	log.Info("start to export metering")

	exports, err := s.exportRepo.ListMetering(ctx)
	if err != nil {
		log.Error("failed to list export metering export", zap.Error(err))
		return err
	}

	for _, export := range exports {
		err = s.meterOneExport(ctx, export)
		if err != nil {
			log.Error("failed to meter export", zap.String("export_id", export.ID), zap.Error(err))
		}
	}

	log.Info("finish export metering")
	return nil
}

func (s *MeteringService) meterOneExport(ctx context.Context, export *entity.Export) (err error) {
	exitReason := ExitReasonNormal
	ctx, span := Tracer.Start(ctx, "metering.meterOneExport")
	meteringTimes := export.MeteringTimes + 1
	var proxyEndpoint string
	var cluster *tidbmgmtservicev1.Cluster
	defer func() {
		tracing.EndSpan(span, err)
		if err != nil {
			exitReason = fmt.Sprintf("%s. meter times: %d", exitReason, meteringTimes)
		}
		// generate metric when success or failed over the max metering times
		if err == nil || meteringTimes >= constant.ExportMaxMeteringTimes {
			updateExportMeterJobMetric(export.ID, exitReason, err)
		}
	}()

	log := utils.GetWorkerLoggerFromContext(ctx).With(zap.String("exportID", export.ID))
	log.Info("start to meter one export")

	// check export again in case unexpected situation
	if export.MeteringStatus != constant.ExportMeteringStateRunning {
		exitReason = "export metering status is not running"
		log.Info("export metering status is not running", zap.String("state", string(export.MeteringStatus)))
		return nil
	}

	// increase the export metering count
	err = s.exportRepo.IncreaseMeteringCount(ctx, export)
	if err != nil {
		exitReason = "increase metering count failed"
		log.Error("increase metering count failed", zap.Error(err))
		return err
	}

	if export.CompletedAt == nil {
		exitReason = "export completed_at is nil"
		log.Error("export completed_at is nil")
		return errors.New("export completed_at is nil")
	}

	clusterIDUint64, _ := strconv.ParseUint(export.ClusterID, 10, 64)
	cluster, err = s.mgmtFacade.GetCluster(
		ctx,
		clusterIDUint64,
		tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC,
	)
	if err != nil {
		exitReason = "failed to get cluster"
		log.Error("failed to get cluster", zap.String("cluster_id", export.ClusterID), zap.Error(err))
		return err
	}
	proxyEndpoint = cluster.GetServerlessCluster().GetCluster().GetProxyEndpoint()

	// update export cr metering annotation to generate metric for console
	var usage int64
	defer func() {
		if err == nil {
			err = s.completeExportMetering(ctx, export, usage, proxyEndpoint)
			if err != nil {
				exitReason = fmt.Sprintf("failed to complete export metering, usage: %d", usage)
				log.Error("failed to complete export metering", zap.Error(err))
			}
		}
	}()
	// check feature gate
	tenantId := export.TenantID
	tenantIdUint64, _ := strconv.ParseUint(tenantId, 10, 64)
	if !s.featureGate.FeatureEnabled(constant.FeatureMetering, tenantIdUint64, export.Region) {
		return nil
	}

	// get export usages
	o11yUrl := config.GetO11yUrl(export.GetCloudProvider(), export.Region, export.GetKubernetes())
	if o11yUrl == "" {
		exitReason = "failed to get o11y url"
		log.Error("failed to get o11y url", zap.String("provider", export.GetCloudProvider()),
			zap.String("region", export.Region), zap.String("k8s", export.GetKubernetes()))
		return errors.New("failed to get o11y url")
	}
	completedAt := *export.CompletedAt
	usage, err = s.o11yFacade.GetExportUsage(ctx, export.ID, o11yUrl, completedAt, constant.MetricTypeExportRU)
	if err != nil {
		log.Error("failed to get export usages", zap.Error(err))
		// get export usages failed may be caused by the short time of the export execution without ru generated
		// it is only a temporary solution to avoid too much alert for the 0 ru export task.
		if export.CreatedAt.Add(constant.ExportAllowMeteringToZeroThreshold).After(completedAt) &&
			meteringTimes >= constant.ExportMaxMeteringTimes {
			log.Warn("set usage to 0 as we can not get metric, maybe ru is not generated", zap.Error(err))
			usage = 0
			return nil
		}
		exitReason = "get export usages failed"
		return err
	}
	log.Info("get export usages", zap.Any("usages", usage))
	if usage == 0 {
		return nil
	}
	// measure export usages
	uuid := fmt.Sprintf("%s/%s/%s/%s", export.Region, export.ClusterID, constant.MetricTypeExportRU, export.ID)
	skuNameBuilder := &vo.SkuNameBuilder{CloudProvider: export.GetCloudProvider(), Region: export.Region}
	var sku string
	if cluster.GetServerlessCluster().GetCluster().GetAutoScaling().GetMinRcu() > 0 {
		sku = skuNameBuilder.Build(constant.SkuTypeRequestCapacityUnit)
		usage /= 60
	} else {
		sku = skuNameBuilder.Build(constant.SkuTypeRequestUnit)
	}
	record := &facade.BillingReport{
		CostID: uuid,
		Cost:   usage,
		Sku:    sku,
	}
	record.TenantID = tenantIdUint64
	record.CostTimeStamp = completedAt.Unix()
	record.Labels = map[string]string{
		constant.BillingLabelTenantID:   tenantId,
		constant.BillingLabelProjectID:  strconv.FormatUint(cluster.GetProjectId(), 10),
		constant.BillingLabelClusterID:  export.ClusterID,
		constant.BillingLabelRegion:     export.Region,
		constant.BillingLabelServerless: "true",
		constant.BillingLabelWorker:     config.GetWorkMode().String(),
	}

	// measure export
	err = s.billingFacade.Measure(ctx, []*facade.BillingReport{record})
	if err != nil {
		log.Error("failed to measure export", zap.Error(err))
		exitReason = "failed to measure export"
		return err
	}

	log.Info("finish meter one export")
	return nil
}

// completeExportMetering persists the metering annotation before initiating CR deletion.
// The Export finalizer keeps the CR alive until the regional controller emits the console metric
// and finishes resource cleanup. Metering is marked successful last so failed Kubernetes operations
// are not recorded as successful.
func (s *MeteringService) completeExportMetering(
	ctx context.Context,
	export *entity.Export,
	usage int64,
	proxyEndpoint string,
) error {
	err := s.updateExportCRMetring(ctx, export, usage, proxyEndpoint)
	if err != nil && !api_errors.IsNotFound(err) {
		return fmt.Errorf("update export CR metering annotation: %w", err)
	}

	// The existing Delete API may have already removed the CR. There is nothing left to delete,
	// but its database metering state still needs to be completed.
	if err == nil {
		projectID, parseErr := strconv.ParseUint(export.ProjectID, 10, 64)
		if parseErr != nil {
			return fmt.Errorf("parse export project ID: %w", parseErr)
		}
		if deleteErr := s.infraFacade.DeleteExport(ctx, proxyEndpoint, projectID, export.ID); deleteErr != nil {
			return fmt.Errorf("delete export CR: %w", deleteErr)
		}
	}

	export.MeteringStatus = constant.ExportMeteringStateSucceed
	if updateErr := s.exportRepo.UpdateMeteringStatus(ctx, export); updateErr != nil {
		return fmt.Errorf("update export metering status: %w", updateErr)
	}
	return nil
}

// updateExportCRMetring update export CR metering annotation.
// Manager will reconcile to calculate the ru showed in console.
func (s *MeteringService) updateExportCRMetring(
	ctx context.Context,
	export *entity.Export,
	usage int64,
	proxyEndpoint string,
) error {
	projectId, err := strconv.ParseUint(export.ProjectID, 10, 64)
	if err != nil {
		return err
	}
	exportCR, err := s.infraFacade.GetExport(ctx, proxyEndpoint, projectId, export.ID)
	if err != nil {
		return err
	}
	if exportCR.Annotations == nil {
		exportCR.Annotations = make(map[string]string)
	}
	exportCR.Annotations[constant.ExportMeteringAnnotation] = strconv.FormatInt(usage, 10)
	err = s.infraFacade.UpdateExport(ctx, proxyEndpoint, exportCR)
	if err != nil {
		return err
	}
	return nil
}
