package meta

import (
	"context"
	"encoding/binary"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/internal/metaerr"
	"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"go.etcd.io/bbolt"
)

func (db *DB) Containers(ctx context.Context) (list []cid.ID, err error) {
	var (
		startedAt = time.Now()
		success   = false
	)
	defer func() {
		db.metrics.AddMethodDuration("Containers", time.Since(startedAt), success)
	}()

	_, span := tracing.StartSpanFromContext(ctx, "metabase.Containers")
	defer span.End()

	db.modeMtx.RLock()
	defer db.modeMtx.RUnlock()

	if db.mode.NoMetabase() {
		return nil, ErrDegradedMode
	}

	err = db.boltDB.View(func(tx *bbolt.Tx) error {
		list, err = db.containers(tx)

		return err
	})
	success = err == nil
	return list, metaerr.Wrap(err)
}

func (db *DB) containers(tx *bbolt.Tx) ([]cid.ID, error) {
	result := make([]cid.ID, 0)
	unique := make(map[string]struct{})
	var cnr cid.ID

	err := tx.ForEach(func(name []byte, _ *bbolt.Bucket) error {
		if parseContainerID(&cnr, name, unique) {
			result = append(result, cnr)
			unique[string(name[1:bucketKeySize])] = struct{}{}
		}

		return nil
	})

	return result, err
}

func (db *DB) ContainerSize(id cid.ID) (size uint64, err error) {
	db.modeMtx.RLock()
	defer db.modeMtx.RUnlock()

	if db.mode.NoMetabase() {
		return 0, ErrDegradedMode
	}

	err = db.boltDB.View(func(tx *bbolt.Tx) error {
		size, err = db.containerSize(tx, id)

		return err
	})

	return size, metaerr.Wrap(err)
}

func (db *DB) containerSize(tx *bbolt.Tx, id cid.ID) (uint64, error) {
	containerVolume := tx.Bucket(containerVolumeBucketName)
	key := make([]byte, cidSize)
	id.Encode(key)

	return parseContainerSize(containerVolume.Get(key)), nil
}

func parseContainerID(dst *cid.ID, name []byte, ignore map[string]struct{}) bool {
	if len(name) != bucketKeySize {
		return false
	}
	if _, ok := ignore[string(name[1:bucketKeySize])]; ok {
		return false
	}
	return dst.Decode(name[1:bucketKeySize]) == nil
}

func parseContainerSize(v []byte) uint64 {
	if len(v) == 0 {
		return 0
	}

	return binary.LittleEndian.Uint64(v)
}

func changeContainerSize(tx *bbolt.Tx, id cid.ID, delta uint64, increase bool) error {
	containerVolume := tx.Bucket(containerVolumeBucketName)
	key := make([]byte, cidSize)
	id.Encode(key)

	size := parseContainerSize(containerVolume.Get(key))

	if increase {
		size += delta
	} else if size > delta {
		size -= delta
	} else {
		size = 0
	}

	buf := make([]byte, 8) // consider using sync.Pool to decrease allocations
	binary.LittleEndian.PutUint64(buf, size)

	return containerVolume.Put(key, buf)
}