package layer

import (
	"context"
	"errors"
	"fmt"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	"go.uber.org/zap"
)

func (n *Layer) GetObjectTagging(ctx context.Context, p *data.GetObjectTaggingParams) (string, map[string]string, error) {
	var err error
	owner := n.BearerOwner(ctx)

	if len(p.ObjectVersion.VersionID) != 0 && p.ObjectVersion.VersionID != data.UnversionedObjectVersionID {
		if tags := n.cache.GetTagging(owner, objectTaggingCacheKey(p.ObjectVersion)); tags != nil {
			return p.ObjectVersion.VersionID, tags, nil
		}
	}

	nodeVersion := p.NodeVersion
	if nodeVersion == nil {
		nodeVersion, err = n.getNodeVersionFromCacheOrFrostfs(ctx, p.ObjectVersion)
		if err != nil {
			return "", nil, err
		}
	}
	p.ObjectVersion.VersionID = nodeVersion.OID.EncodeToString()

	if tags := n.cache.GetTagging(owner, objectTaggingCacheKey(p.ObjectVersion)); tags != nil {
		return p.ObjectVersion.VersionID, tags, nil
	}

	tags, err := n.treeService.GetObjectTagging(ctx, p.ObjectVersion.BktInfo, nodeVersion)
	if err != nil {
		if errors.Is(err, tree.ErrNodeNotFound) {
			return "", nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchKey), err.Error())
		}
		return "", nil, err
	}

	n.cache.PutTagging(owner, objectTaggingCacheKey(p.ObjectVersion), tags)

	return p.ObjectVersion.VersionID, tags, nil
}

func (n *Layer) PutObjectTagging(ctx context.Context, p *data.PutObjectTaggingParams) (err error) {
	nodeVersion := p.NodeVersion
	if nodeVersion == nil {
		nodeVersion, err = n.getNodeVersionFromCacheOrFrostfs(ctx, p.ObjectVersion)
		if err != nil {
			return err
		}
	}
	p.ObjectVersion.VersionID = nodeVersion.OID.EncodeToString()

	err = n.treeService.PutObjectTagging(ctx, p.ObjectVersion.BktInfo, nodeVersion, p.TagSet)
	if err != nil {
		if errors.Is(err, tree.ErrNodeNotFound) {
			return fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchKey), err.Error())
		}
		return err
	}

	n.cache.PutTagging(n.BearerOwner(ctx), objectTaggingCacheKey(p.ObjectVersion), p.TagSet)

	return nil
}

func (n *Layer) DeleteObjectTagging(ctx context.Context, p *data.ObjectVersion) error {
	version, err := n.getNodeVersion(ctx, p)
	if err != nil {
		return err
	}

	err = n.treeService.DeleteObjectTagging(ctx, p.BktInfo, version)
	if err != nil {
		if errors.Is(err, tree.ErrNodeNotFound) {
			return fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchKey), err.Error())
		}
		return err
	}

	p.VersionID = version.OID.EncodeToString()

	n.cache.DeleteTagging(objectTaggingCacheKey(p))

	return nil
}

func (n *Layer) GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) (map[string]string, error) {
	owner := n.BearerOwner(ctx)

	if tags := n.cache.GetTagging(owner, bucketTaggingCacheKey(bktInfo.CID)); tags != nil {
		return tags, nil
	}

	tags, err := n.treeService.GetBucketTagging(ctx, bktInfo)
	if err != nil && !errors.Is(err, tree.ErrNodeNotFound) {
		return nil, err
	}

	n.cache.PutTagging(owner, bucketTaggingCacheKey(bktInfo.CID), tags)

	return tags, nil
}

func (n *Layer) PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo, tagSet map[string]string) error {
	if err := n.treeService.PutBucketTagging(ctx, bktInfo, tagSet); err != nil {
		return err
	}

	n.cache.PutTagging(n.BearerOwner(ctx), bucketTaggingCacheKey(bktInfo.CID), tagSet)

	return nil
}

func (n *Layer) DeleteBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) error {
	n.cache.DeleteTagging(bucketTaggingCacheKey(bktInfo.CID))

	return n.treeService.DeleteBucketTagging(ctx, bktInfo)
}

func objectTaggingCacheKey(p *data.ObjectVersion) string {
	return ".tagset." + p.BktInfo.CID.EncodeToString() + "." + p.ObjectName + "." + p.VersionID
}

func bucketTaggingCacheKey(cnrID cid.ID) string {
	return ".tagset." + cnrID.EncodeToString()
}

func (n *Layer) getNodeVersion(ctx context.Context, objVersion *data.ObjectVersion) (*data.NodeVersion, error) {
	var err error
	var version *data.NodeVersion

	if objVersion.VersionID == data.UnversionedObjectVersionID {
		version, err = n.treeService.GetUnversioned(ctx, objVersion.BktInfo, objVersion.ObjectName)
	} else if len(objVersion.VersionID) == 0 {
		version, err = n.treeService.GetLatestVersion(ctx, objVersion.BktInfo, objVersion.ObjectName)
	} else {
		versions, err2 := n.treeService.GetVersions(ctx, objVersion.BktInfo, objVersion.ObjectName)
		if err2 != nil {
			return nil, err2
		}
		for _, v := range versions {
			if v.OID.EncodeToString() == objVersion.VersionID {
				version = v
				break
			}
		}
		if version == nil {
			err = fmt.Errorf("%w: there isn't tree node with requested version id", apierr.GetAPIError(apierr.ErrNoSuchVersion))
		}
	}

	if err == nil && version.IsDeleteMarker && !objVersion.NoErrorOnDeleteMarker {
		return nil, fmt.Errorf("%w: found version is delete marker", apierr.GetAPIError(apierr.ErrNoSuchKey))
	} else if errors.Is(err, tree.ErrNodeNotFound) {
		return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchKey), err.Error())
	}

	if err == nil && version != nil && !version.IsDeleteMarker {
		n.reqLogger(ctx).Debug(logs.GetTreeNode,
			zap.Stringer("cid", objVersion.BktInfo.CID), zap.Stringer("oid", version.OID))
	}

	return version, err
}

func (n *Layer) getNodeVersionFromCache(owner user.ID, o *data.ObjectVersion) *data.NodeVersion {
	if len(o.VersionID) == 0 || o.VersionID == data.UnversionedObjectVersionID {
		return nil
	}

	var objID oid.ID
	if objID.DecodeString(o.VersionID) != nil {
		return nil
	}

	var addr oid.Address
	addr.SetContainer(o.BktInfo.CID)
	addr.SetObject(objID)

	extObjectInfo := n.cache.GetObject(owner, addr)
	if extObjectInfo == nil {
		return nil
	}

	return extObjectInfo.NodeVersion
}