package cron

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
	meteringsdkcommon "github.com/pingcap/metering_sdk/common"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

type meteringWriter interface {
	Write(ctx context.Context, data any) error
}

type ManualBackupMeteringJob struct {
	manualBackupRepo   repo.ManualBackupRepo
	lockRepo           repo.LockRepo
	expression         string
	meteringDataWriter meteringWriter
}

func NewManualBackupMeteringJob(
	manualBackupRepo repo.ManualBackupRepo,
	lockRepo repo.LockRepo,
	meteringDataWriter meteringWriter,
	expression string,
) *ManualBackupMeteringJob {
	return &ManualBackupMeteringJob{
		manualBackupRepo:   manualBackupRepo,
		lockRepo:           lockRepo,
		expression:         expression,
		meteringDataWriter: meteringDataWriter,
	}
}

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

func (j *ManualBackupMeteringJob) CronExpression() string {
	return j.expression
}

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

	backups, err := j.manualBackupRepo.ListUndeletedSucceeded(ctx)
	if err != nil {
		tilog.Error(ctx, "failed to list undeleted succeeded backups", zap.Error(err))
		return fmt.Errorf("failed to run manual backup metering: %w", err)
	}

	if len(backups) == 0 {
		tilog.Info(ctx, "no active manual backups to meter")
		return nil
	}

	grouped := make(map[string][]*entity.ManualBackup)
	for _, b := range backups {
		grouped[b.SharedPoolID] = append(grouped[b.SharedPoolID], b)
	}

	var hasError bool
	for sharedPoolID, poolBackups := range grouped {
		meteringData := &meteringsdkcommon.MeteringData{
			Timestamp:    time.Now().Unix() / 60 * 60,
			Category:     "manual-backup",
			SelfID:       strings.ReplaceAll(uuid.NewString(), "-", ""),
			SharedPoolID: sharedPoolID,
			Data:         make([]map[string]any, 0, len(poolBackups)),
		}
		for _, b := range poolBackups {
			row := map[string]any{
				"cluster_id": b.ClusterID,
				"backup_id":  b.BackupID,
				// use cluster size metering instead of backup artifact size
				"backup_size_gb": &meteringsdkcommon.MeteringValue{Value: bytesToGB(b.RestoreSize), Unit: "GB"},
			}
			meteringData.Data = append(meteringData.Data, row)
		}
		if err := j.meteringDataWriter.Write(ctx, meteringData); err != nil {
			tilog.Error(ctx, "failed to write metering data for shared pool",
				zap.String("sharedPoolID", sharedPoolID),
				zap.Error(err))
			hasError = true
			continue
		}
		tilog.Info(ctx, "wrote manual backup metering data",
			zap.String("sharedPoolID", sharedPoolID),
			zap.Int("backupCount", len(poolBackups)))
	}

	if hasError {
		return fmt.Errorf("failed to write metering data for one or more shared pools")
	}
	return nil
}

func (j *ManualBackupMeteringJob) lock(ctx context.Context) (func() error, error) {
	key := "manual_backup_metering"
	lock := &entity.Lock{
		Key:         key,
		TimeoutSecs: 10,
	}
	unlockFunc, err := j.lockRepo.Lock(ctx, lock)
	if err != nil {
		return nil, err
	}
	return unlockFunc, nil
}

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