package container

import (
	"sync"

	utilSync "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/sync"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
)

type Info struct {
	Indexed bool
	Removed bool
}

type infoValue struct {
	info Info
	err  error
}

type InfoProvider interface {
	Info(id cid.ID) (Info, error)
}

type infoProvider struct {
	mtx   *sync.RWMutex
	cache map[cid.ID]infoValue
	kl    *utilSync.KeyLocker[cid.ID]

	source        Source
	sourceErr     error
	sourceOnce    *sync.Once
	sourceFactory func() (Source, error)
}

func NewInfoProvider(sourceFactory func() (Source, error)) InfoProvider {
	return &infoProvider{
		mtx:           &sync.RWMutex{},
		cache:         make(map[cid.ID]infoValue),
		sourceOnce:    &sync.Once{},
		kl:            utilSync.NewKeyLocker[cid.ID](),
		sourceFactory: sourceFactory,
	}
}

func (r *infoProvider) Info(id cid.ID) (Info, error) {
	v, found := r.tryGetFromCache(id)
	if found {
		return v.info, v.err
	}

	return r.getFromSource(id)
}

func (r *infoProvider) tryGetFromCache(id cid.ID) (infoValue, bool) {
	r.mtx.RLock()
	defer r.mtx.RUnlock()

	value, found := r.cache[id]
	return value, found
}

func (r *infoProvider) getFromSource(id cid.ID) (Info, error) {
	r.kl.Lock(id)
	defer r.kl.Unlock(id)

	if v, ok := r.tryGetFromCache(id); ok {
		return v.info, v.err
	}

	r.sourceOnce.Do(func() {
		r.source, r.sourceErr = r.sourceFactory()
	})
	if r.sourceErr != nil {
		return Info{}, r.sourceErr
	}

	cnr, err := r.source.Get(id)
	var civ infoValue
	if err != nil {
		if client.IsErrContainerNotFound(err) {
			removed, err := WasRemoved(r.source, id)
			if err != nil {
				civ.err = err
			} else {
				civ.info.Removed = removed
			}
		} else {
			civ.err = err
		}
	} else {
		civ.info.Indexed = IsIndexedContainer(cnr.Value)
	}
	r.putToCache(id, civ)
	return civ.info, civ.err
}

func (r *infoProvider) putToCache(id cid.ID, ct infoValue) {
	r.mtx.Lock()
	defer r.mtx.Unlock()

	r.cache[id] = ct
}