package engine

import (
	"context"
	"sync"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/shard"
	"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
	"golang.org/x/sync/errgroup"
)

type RebuildPrm struct {
	ShardIDs          []*shard.ID
	ConcurrencyLimit  uint32
	TargetFillPercent uint32
}

type ShardRebuildResult struct {
	ShardID  *shard.ID
	Success  bool
	ErrorMsg string
}

type RebuildRes struct {
	ShardResults []ShardRebuildResult
}

func (e *StorageEngine) Rebuild(ctx context.Context, prm RebuildPrm) (RebuildRes, error) {
	ctx, span := tracing.StartSpanFromContext(ctx, "StorageEngine.Rebuild",
		trace.WithAttributes(
			attribute.Int("shard_id_count", len(prm.ShardIDs)),
			attribute.Int64("target_fill_percent", int64(prm.TargetFillPercent)),
			attribute.Int64("concurrency_limit", int64(prm.ConcurrencyLimit)),
		))
	defer span.End()

	res := RebuildRes{
		ShardResults: make([]ShardRebuildResult, 0, len(prm.ShardIDs)),
	}
	resGuard := &sync.Mutex{}

	concLimiter := &concurrencyLimiter{semaphore: make(chan struct{}, prm.ConcurrencyLimit)}

	eg, egCtx := errgroup.WithContext(ctx)
	for _, shardID := range prm.ShardIDs {
		eg.Go(func() error {
			e.mtx.RLock()
			sh, ok := e.shards[shardID.String()]
			e.mtx.RUnlock()

			if !ok {
				resGuard.Lock()
				defer resGuard.Unlock()
				res.ShardResults = append(res.ShardResults, ShardRebuildResult{
					ShardID:  shardID,
					ErrorMsg: errShardNotFound.Error(),
				})
				return nil
			}

			err := sh.ScheduleRebuild(egCtx, shard.RebuildPrm{
				ConcurrencyLimiter: concLimiter,
				TargetFillPercent:  prm.TargetFillPercent,
			})

			resGuard.Lock()
			defer resGuard.Unlock()

			if err != nil {
				res.ShardResults = append(res.ShardResults, ShardRebuildResult{
					ShardID:  shardID,
					ErrorMsg: err.Error(),
				})
			} else {
				res.ShardResults = append(res.ShardResults, ShardRebuildResult{
					ShardID: shardID,
					Success: true,
				})
			}
			return nil
		})
	}

	if err := eg.Wait(); err != nil {
		return RebuildRes{}, err
	}
	return res, nil
}

type concurrencyLimiter struct {
	semaphore chan struct{}
}

func (l *concurrencyLimiter) AcquireWorkSlot(ctx context.Context) (common.ReleaseFunc, error) {
	select {
	case l.semaphore <- struct{}{}:
		return l.releaseWorkSlot, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

func (l *concurrencyLimiter) releaseWorkSlot() {
	<-l.semaphore
}