package server

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"time"

	meteringsdkcommon "github.com/pingcap/metering_sdk/common"
	meteringsdkwriter "github.com/pingcap/metering_sdk/writer"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	flexBackupMeteringCategory = "flex-backup"
	flexBackupMeteringSKUValue = "daily"

	maxAutoBackupMetadataConflictAttempts = 16
)

func (s *serverImpl) UpdateAutoBackupMetering(ctx context.Context, req *apiv1.UpdateAutoBackupMeteringRequest) error {
	if req == nil {
		return fmt.Errorf("update auto backup metering request is nil")
	}
	if err := s.guardTiDBClusterMutationByID(ctx, req.GetClusterId(), sharedPoolMutationBlockedWhilePaused); err != nil {
		return err
	}
	tilog.Info(ctx, "handling update auto backup metering request",
		zap.String("clusterID", req.GetClusterId()),
		zap.String("mode", req.GetMode().String()),
		zap.String("skuName", req.GetSkuName()),
		zap.String("skuValue", req.GetSkuValue()),
	)
	if req.GetMode() == apiv1.AutoBackupMode_FLEXIBLE_RETENTION {
		tilog.Info(ctx, "writing flexible auto backup metering metadata",
			zap.String("clusterID", req.GetClusterId()),
			zap.String("category", flexBackupMeteringCategory),
			zap.String("skuName", req.GetSkuName()),
			zap.String("skuValue", flexBackupMeteringSKUValue),
		)
		return s.writeAutoBackupMetadata(ctx, req.GetClusterId(), "backup", req.GetSkuName(), flexBackupMeteringSKUValue)
	}
	tilog.Info(ctx, "writing standard auto backup metering metadata",
		zap.String("clusterID", req.GetClusterId()),
		zap.String("category", "backup"),
		zap.String("skuName", req.GetSkuName()),
		zap.String("skuValue", req.GetSkuValue()),
	)
	return s.writeAutoBackupMetadata(ctx, req.GetClusterId(), "backup", req.GetSkuName(), req.GetSkuValue())
}

func (s *serverImpl) ReportAutoBackupMetering(ctx context.Context, req *apiv1.ReportAutoBackupMeteringRequest) error {
	if req == nil {
		return fmt.Errorf("report auto backup metering request is nil")
	}
	tilog.Info(ctx, "handling report auto backup metering request",
		zap.String("clusterID", req.GetClusterId()),
		zap.String("sharedPoolID", req.GetSharedPoolId()),
		zap.Int64("timestamp", req.GetTimestamp()),
		zap.Int("backupCount", len(req.GetBackups())),
	)
	if s.meteringDataWriter == nil {
		tilog.Info(ctx, "metering writer is not initialized, skip reporting auto backup metering", zap.String("clusterID", req.GetClusterId()))
		return nil
	}
	if req.GetClusterId() == "" {
		return fmt.Errorf("cluster id is required")
	}
	if req.GetSharedPoolId() == "" {
		return fmt.Errorf("shared pool id is required")
	}
	if len(req.GetBackups()) == 0 {
		return nil
	}
	timestamp := req.GetTimestamp()
	if timestamp == 0 {
		timestamp = time.Now().UTC().Truncate(time.Minute).Unix()
	}
	rows := make([]map[string]any, 0, len(req.GetBackups()))
	seen := make(map[string]struct{}, len(req.GetBackups()))
	for _, backup := range req.GetBackups() {
		if backup.GetBackupId() == "" {
			return fmt.Errorf("backup id is required")
		}
		if _, ok := seen[backup.GetBackupId()]; ok {
			continue
		}
		seen[backup.GetBackupId()] = struct{}{}
		rows = append(rows, map[string]any{
			"cluster_id":     req.GetClusterId(),
			"backup_id":      backup.GetBackupId(),
			"backup_size_gb": &meteringsdkcommon.MeteringValue{Value: autoBackupMeteringBytesToGB(backup.GetBackupSizeBytes()), Unit: "GB"},
		})
	}
	if len(rows) == 0 {
		return nil
	}
	payload := &meteringsdkcommon.MeteringData{
		Timestamp:    timestamp,
		Category:     flexBackupMeteringCategory,
		SelfID:       autoBackupMeteringSelfID(req.GetSharedPoolId(), req.GetClusterId(), timestamp),
		SharedPoolID: req.GetSharedPoolId(),
		Data:         rows,
	}
	if err := s.meteringDataWriter.Write(ctx, payload); err != nil {
		tilog.Error(ctx, "failed to write auto backup metering data", zap.Error(err))
		return err
	}
	tilog.Info(ctx, "wrote flexible auto backup metering data",
		zap.String("clusterID", req.GetClusterId()),
		zap.String("sharedPoolID", req.GetSharedPoolId()),
		zap.String("category", flexBackupMeteringCategory),
		zap.Int64("timestamp", timestamp),
		zap.Int("backupCount", len(rows)),
	)
	return nil
}

func autoBackupMeteringBytesToGB(bytes uint64) uint64 {
	const gb = 1073741824
	q := bytes / gb
	if bytes%gb != 0 {
		q++
	}
	return q
}

func autoBackupMeteringSelfID(sharedPoolID, clusterID string, timestamp int64) string {
	sum := sha256.Sum256([]byte(fmt.Sprintf("auto-backup/%s/%s/%d", sharedPoolID, clusterID, timestamp)))
	return hex.EncodeToString(sum[:16])
}

func (s *serverImpl) writeAutoBackupMetadata(ctx context.Context, clusterID, category, skuName, skuValue string) error {
	if s.meteringWriter == nil {
		tilog.Info(ctx, "metering writer is not initialized, skip updating auto backup metering",
			zap.String("clusterID", clusterID),
			zap.String("category", category),
			zap.String("skuName", skuName),
			zap.String("skuValue", skuValue),
		)
		return nil
	}
	data := &meteringsdkcommon.MetaData{
		ClusterID: clusterID, Type: meteringsdkcommon.MetaTypeLogic,
		Category: category, Metadata: map[string]any{skuName: skuValue},
	}
	baseModifyTS := time.Now().Unix()
	for attempt := 0; attempt < maxAutoBackupMetadataConflictAttempts; attempt++ {
		data.ModifyTS = baseModifyTS + int64(attempt)
		if err := s.meteringWriter.Write(ctx, data); err != nil {
			if errors.Is(err, meteringsdkwriter.ErrFileExists) {
				tilog.Warn(ctx, "auto backup metering metadata path already exists, retry with next modify timestamp",
					zap.String("clusterID", clusterID),
					zap.String("category", category),
					zap.String("skuName", skuName),
					zap.String("skuValue", skuValue),
					zap.Int64("modifyTS", data.ModifyTS),
					zap.Int("attempt", attempt+1),
					zap.Error(err),
				)
				if ctxErr := ctx.Err(); ctxErr != nil {
					return ctxErr
				}
				continue
			}
			tilog.Error(ctx, "failed to write auto backup metering metadata",
				zap.String("clusterID", clusterID),
				zap.String("category", category),
				zap.String("skuName", skuName),
				zap.String("skuValue", skuValue),
				zap.Int64("modifyTS", data.ModifyTS),
				zap.Error(err),
			)
			return err
		}
		tilog.Info(ctx, "wrote auto backup metering metadata",
			zap.String("clusterID", clusterID),
			zap.String("category", category),
			zap.String("skuName", skuName),
			zap.String("skuValue", skuValue),
			zap.Int64("modifyTS", data.ModifyTS),
		)
		return nil
	}
	return fmt.Errorf("failed to write auto backup metering metadata after %d file-exists conflicts", maxAutoBackupMetadataConflictAttempts)
}
