package storage

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"math/rand"
	"path"
	"strings"
	"time"

	dockerList "github.com/distribution/distribution/v3/manifest/manifestlist"
	docker "github.com/distribution/distribution/v3/manifest/schema2"
	"github.com/distribution/distribution/v3/registry/storage/driver"
	godigest "github.com/opencontainers/go-digest"
	"github.com/opencontainers/image-spec/schema"
	imeta "github.com/opencontainers/image-spec/specs-go"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/santhosh-tekuri/jsonschema/v5"

	zerr "zotregistry.dev/zot/errors"
	zcommon "zotregistry.dev/zot/pkg/common"
	"zotregistry.dev/zot/pkg/compat"
	"zotregistry.dev/zot/pkg/extensions/monitoring"
	zlog "zotregistry.dev/zot/pkg/log"
	"zotregistry.dev/zot/pkg/scheduler"
	storageConstants "zotregistry.dev/zot/pkg/storage/constants"
	storageTypes "zotregistry.dev/zot/pkg/storage/types"
)

const (
	manifestWithEmptyLayersErrMsg = "layers/minItems: minimum 1 items required, but found 0 items"
	cosignSignatureTagSuffix      = "sig"
)

func GetTagsByIndex(index ispec.Index) []string {
	tags := make([]string, 0)

	for _, manifest := range index.Manifests {
		v, ok := manifest.Annotations[ispec.AnnotationRefName]
		if ok {
			tags = append(tags, v)
		}
	}

	return tags
}

func GetManifestDescByReference(index ispec.Index, reference string) (ispec.Descriptor, bool) {
	var manifestDesc ispec.Descriptor

	for _, manifest := range index.Manifests {
		if reference == manifest.Digest.String() {
			return manifest, true
		}

		v, ok := manifest.Annotations[ispec.AnnotationRefName]
		if ok && v == reference {
			return manifest, true
		}
	}

	return manifestDesc, false
}

func ValidateManifest(imgStore storageTypes.ImageStore, repo, reference, mediaType string, body []byte,
	compats []compat.MediaCompatibility, log zlog.Logger,
) error {
	// validate the manifest
	if !IsSupportedMediaType(compats, mediaType) {
		log.Debug().Interface("actual", mediaType).
			Msg("bad manifest media type")

		return zerr.ErrBadManifest
	}

	if len(body) == 0 {
		log.Debug().Int("len", len(body)).Msg("invalid body length")

		return zerr.ErrBadManifest
	}

	switch mediaType {
	case ispec.MediaTypeImageManifest:
		var manifest ispec.Manifest

		// validate manifest
		if err := ValidateManifestSchema(body); err != nil {
			log.Error().Err(err).Msg("failed to validate OCIv1 image manifest schema")

			return zerr.NewError(zerr.ErrBadManifest).AddDetail("jsonSchemaValidation", err.Error())
		}

		if err := json.Unmarshal(body, &manifest); err != nil {
			log.Error().Err(err).Msg("failed to unmarshal JSON")

			return zerr.ErrBadManifest
		}

		// validate blobs only for known media types
		if manifest.Config.MediaType == ispec.MediaTypeImageConfig ||
			manifest.Config.MediaType == ispec.MediaTypeEmptyJSON {
			// validate config blob - a lightweight check if the blob is present
			ok, _, _, err := imgStore.StatBlob(repo, manifest.Config.Digest)
			if !ok || err != nil {
				log.Error().Err(err).Str("digest", manifest.Config.Digest.String()).
					Msg("failed to stat blob due to missing config blob")

				return zerr.ErrBadManifest
			}

			// validate layers - a lightweight check if the blob is present
			for _, layer := range manifest.Layers {
				if IsNonDistributable(layer.MediaType) {
					log.Debug().Str("digest", layer.Digest.String()).Str("mediaType", layer.MediaType).
						Msg("skip checking non-distributable layer exists")

					continue
				}

				ok, _, _, err := imgStore.StatBlob(repo, layer.Digest)
				if !ok || err != nil {
					log.Error().Err(err).Str("digest", layer.Digest.String()).
						Msg("failed to validate manifest due to missing layer blob")

					return zerr.ErrBadManifest
				}
			}
		}
	case ispec.MediaTypeImageIndex:
		// validate manifest
		if err := ValidateImageIndexSchema(body); err != nil {
			log.Error().Err(err).Msg("failed to validate OCIv1 image index manifest schema")

			return zerr.NewError(zerr.ErrBadManifest).AddDetail("jsonSchemaValidation", err.Error())
		}

		var indexManifest ispec.Index
		if err := json.Unmarshal(body, &indexManifest); err != nil {
			log.Error().Err(err).Msg("failed to unmarshal JSON")

			return zerr.ErrBadManifest
		}

		for _, manifest := range indexManifest.Manifests {
			if ok, _, _, err := imgStore.StatBlob(repo, manifest.Digest); !ok || err != nil {
				log.Error().Err(err).Str("digest", manifest.Digest.String()).
					Msg("failed to stat manifest due to missing manifest blob")

				return zerr.ErrBadManifest
			}
		}
	default:
		// non-OCI compatible
		descriptors, err := compat.Validate(body, mediaType)
		if err != nil {
			log.Error().Err(err).Msg("failed to unmarshal JSON")

			return zerr.ErrBadManifest
		}

		for _, desc := range descriptors {
			if ok, _, _, err := imgStore.StatBlob(repo, desc.Digest); !ok || err != nil {
				log.Error().Err(err).Str("digest", desc.Digest.String()).
					Msg("failed to stat non-OCI descriptor due to missing blob")

				return zerr.ErrBadManifest
			}
		}
	}

	return nil
}

// Returns the canonical digest or the digest provided by the reference if any
// Per spec, the canonical digest would always be returned to the client in
// request headers, but that does not make sense if the client requested a different digest algorithm
// See https://github.com/opencontainers/distribution-spec/issues/494
func GetAndValidateRequestDigest(body []byte, reference string, log zlog.Logger) (
	godigest.Digest, error,
) {
	expectedDigest, err := godigest.Parse(reference)
	if err != nil {
		// This is a non-digest reference
		return godigest.Canonical.FromBytes(body), err
	}

	actualDigest := expectedDigest.Algorithm().FromBytes(body)

	if expectedDigest.String() != actualDigest.String() {
		log.Error().Str("actual", actualDigest.String()).Str("expected", expectedDigest.String()).
			Msg("failed to validate manifest digest")

		return actualDigest, zerr.ErrBadManifest
	}

	return actualDigest, nil
}

/*
CheckIfIndexNeedsUpdate verifies if an index needs to be updated given a new manifest descriptor.

Returns whether or not index needs update, in the latter case it will also return the previous digest.
*/
func CheckIfIndexNeedsUpdate(index *ispec.Index, desc *ispec.Descriptor,
	log zlog.Logger,
) (bool, godigest.Digest, error) {
	var oldDgst godigest.Digest

	var reference string

	tag, ok := desc.Annotations[ispec.AnnotationRefName]
	if ok {
		reference = tag
	} else {
		reference = desc.Digest.String()
	}

	updateIndex := true

	for midx, manifest := range index.Manifests {
		manifest := manifest
		if reference == manifest.Digest.String() {
			// nothing changed, so don't update
			updateIndex = false

			break
		}

		v, ok := manifest.Annotations[ispec.AnnotationRefName]
		if ok && v == reference {
			if manifest.Digest.String() == desc.Digest.String() {
				// nothing changed, so don't update
				updateIndex = false

				break
			}

			// manifest contents have changed for the same tag,
			// so update index.json descriptor
			log.Info().
				Int64("old size", manifest.Size).
				Int64("new size", desc.Size).
				Str("old digest", manifest.Digest.String()).
				Str("new digest", desc.Digest.String()).
				Str("old mediaType", manifest.MediaType).
				Str("new mediaType", desc.MediaType).
				Msg("updating existing tag with new manifest contents")

			// changing media-type is disallowed!
			if manifest.MediaType != desc.MediaType {
				log.Info().
					Str("old mediaType", manifest.MediaType).
					Str("new mediaType", desc.MediaType).Msg("media-type changed")
			}

			oldDesc := *desc

			desc = &manifest
			oldDgst = manifest.Digest
			desc.Size = oldDesc.Size
			desc.Digest = oldDesc.Digest

			index.Manifests = append(index.Manifests[:midx], index.Manifests[midx+1:]...)

			break
		}
	}

	return updateIndex, oldDgst, nil
}

// GetIndex returns the contents of index.json.
func GetIndex(imgStore storageTypes.ImageStore, repo string, log zlog.Logger) (ispec.Index, error) {
	var index ispec.Index

	buf, err := imgStore.GetIndexContent(repo)
	if err != nil {
		if errors.As(err, &driver.PathNotFoundError{}) {
			return index, zerr.ErrRepoNotFound
		}

		return index, err
	}

	if err := json.Unmarshal(buf, &index); err != nil {
		log.Error().Err(err).Str("dir", path.Join(imgStore.RootDir(), repo)).Msg("invalid JSON")

		return index, zerr.ErrRepoBadVersion
	}

	return index, nil
}

// GetImageIndex returns a multiarch type image.
func GetImageIndex(imgStore storageTypes.ImageStore, repo string, digest godigest.Digest, log zlog.Logger,
) (ispec.Index, error) {
	var imageIndex ispec.Index

	if err := digest.Validate(); err != nil {
		return imageIndex, err
	}

	buf, err := imgStore.GetBlobContent(repo, digest)
	if err != nil {
		return imageIndex, err
	}

	indexPath := path.Join(imgStore.RootDir(), repo, "blobs",
		digest.Algorithm().String(), digest.Encoded())

	if err := json.Unmarshal(buf, &imageIndex); err != nil {
		log.Error().Err(err).Str("path", indexPath).Msg("invalid JSON")

		return imageIndex, err
	}

	return imageIndex, nil
}

func GetImageManifest(imgStore storageTypes.ImageStore, repo string, digest godigest.Digest, log zlog.Logger,
) (ispec.Manifest, error) {
	var manifestContent ispec.Manifest

	manifestBlob, err := imgStore.GetBlobContent(repo, digest)
	if err != nil {
		return manifestContent, err
	}

	manifestPath := path.Join(imgStore.RootDir(), repo, "blobs",
		digest.Algorithm().String(), digest.Encoded())

	if err := json.Unmarshal(manifestBlob, &manifestContent); err != nil {
		log.Error().Err(err).Str("path", manifestPath).Msg("invalid JSON")

		return manifestContent, err
	}

	return manifestContent, nil
}

func RemoveManifestDescByReference(index *ispec.Index, reference string, detectCollisions bool,
) (ispec.Descriptor, error) {
	var removedManifest ispec.Descriptor

	var found, foundAsTag bool                        // keep track if the manifest was found by digest or by tag
	manifestDigestCounts := map[godigest.Digest]int{} // Keep track of the number of references for a specific digest

	foundCount := 0

	var outIndex ispec.Index

	for _, manifest := range index.Manifests {
		manifestDigestCounts[manifest.Digest]++

		tag, ok := manifest.Annotations[ispec.AnnotationRefName]
		if ok && tag == reference {
			removedManifest = manifest
			found = true
			foundAsTag = true
			foundCount++

			continue
		} else if reference == manifest.Digest.String() {
			removedManifest = manifest
			found = true
			foundCount++

			continue
		}

		outIndex.Manifests = append(outIndex.Manifests, manifest)
	}

	if foundCount > 1 && detectCollisions {
		return ispec.Descriptor{}, zerr.ErrManifestConflict
	} else if !found {
		return ispec.Descriptor{}, zerr.ErrManifestNotFound
	}

	// In case of delete by digest we remove the manifest right away from storage
	// but in case of delete by tag we want to only remove the tag
	// and handle the manifest based on retention rules later.
	// If there are more than one tags with the same digest we want to keep the others.
	// If and only if there are no other tags except the one we want to remove
	// we need to add a new descriptor without a tag name to keep track of
	// the manifest in index.json so it remains accessible by digest.
	if foundAsTag && manifestDigestCounts[removedManifest.Digest] == 1 {
		newManifest := removedManifest
		delete(newManifest.Annotations, ispec.AnnotationRefName)
		outIndex.Manifests = append(outIndex.Manifests, newManifest)
	}

	index.Manifests = outIndex.Manifests

	return removedManifest, nil
}

/*
Unmarshal an image index and for all manifests in that
index, ensure that they do not have a name or they are not in other
manifest indexes else GC can never clean them.
*/
func UpdateIndexWithPrunedImageManifests(imgStore storageTypes.ImageStore, index *ispec.Index, repo string,
	desc ispec.Descriptor, oldDgst godigest.Digest, log zlog.Logger,
) error {
	if (desc.MediaType == ispec.MediaTypeImageIndex) && (oldDgst != "") {
		otherImgIndexes := []ispec.Descriptor{}

		for _, manifest := range index.Manifests {
			if manifest.MediaType == ispec.MediaTypeImageIndex {
				otherImgIndexes = append(otherImgIndexes, manifest)
			}
		}

		otherImgIndexes = append(otherImgIndexes, desc)

		prunedManifests, err := PruneImageManifestsFromIndex(imgStore, repo, oldDgst, *index, otherImgIndexes, log)
		if err != nil {
			return err
		}

		index.Manifests = prunedManifests
	}

	return nil
}

/*
Before an image index manifest is pushed to a repo, its constituent manifests
are pushed first, so when updating/removing this image index manifest, we also
need to determine if there are other image index manifests which refer to the
same constitutent manifests so that they can be garbage-collected correctly

PruneImageManifestsFromIndex is a helper routine to achieve this.
*/
func PruneImageManifestsFromIndex(imgStore storageTypes.ImageStore, repo string, digest godigest.Digest, //nolint:gocyclo,lll
	outIndex ispec.Index, otherImgIndexes []ispec.Descriptor, log zlog.Logger,
) ([]ispec.Descriptor, error) {
	dir := path.Join(imgStore.RootDir(), repo)

	indexPath := path.Join(dir, "blobs", digest.Algorithm().String(), digest.Encoded())

	buf, err := imgStore.GetBlobContent(repo, digest)
	if err != nil {
		return nil, err
	}

	var imgIndex ispec.Index
	if err := json.Unmarshal(buf, &imgIndex); err != nil {
		log.Error().Err(err).Str("path", indexPath).Msg("invalid JSON")

		return nil, err
	}

	inUse := map[string]uint{}

	for _, manifest := range imgIndex.Manifests {
		inUse[manifest.Digest.Encoded()]++
	}

	for _, otherIndex := range otherImgIndexes {
		oindex, err := GetImageIndex(imgStore, repo, otherIndex.Digest, log)
		if err != nil {
			return nil, err
		}

		for _, omanifest := range oindex.Manifests {
			_, ok := inUse[omanifest.Digest.Encoded()]
			if ok {
				inUse[omanifest.Digest.Encoded()]++
			}
		}
	}

	prunedManifests := []ispec.Descriptor{}

	// for all manifests in the index, skip those that either have a tag or
	// are used in other imgIndexes
	for _, outManifest := range outIndex.Manifests {
		if outManifest.MediaType != ispec.MediaTypeImageManifest {
			prunedManifests = append(prunedManifests, outManifest)

			continue
		}

		_, ok := outManifest.Annotations[ispec.AnnotationRefName]
		if ok {
			prunedManifests = append(prunedManifests, outManifest)

			continue
		}

		count, ok := inUse[outManifest.Digest.Encoded()]
		if !ok {
			prunedManifests = append(prunedManifests, outManifest)

			continue
		}

		if count != 1 {
			// this manifest is in use in other image indexes
			prunedManifests = append(prunedManifests, outManifest)

			continue
		}
	}

	return prunedManifests, nil
}

func isBlobReferencedInImageManifest(imgStore storageTypes.ImageStore, repo string,
	bdigest, mdigest godigest.Digest, log zlog.Logger,
) (bool, error) {
	if bdigest == mdigest {
		return true, nil
	}

	manifestContent, err := GetImageManifest(imgStore, repo, mdigest, log)
	if err != nil {
		log.Error().Err(err).Str("repo", repo).Str("digest", mdigest.String()).Str("component", "gc").
			Msg("failed to read manifest image")

		return false, err
	}

	if bdigest == manifestContent.Config.Digest {
		return true, nil
	}

	for _, layer := range manifestContent.Layers {
		if bdigest == layer.Digest {
			return true, nil
		}
	}

	return false, nil
}

func IsBlobReferencedInImageIndex(imgStore storageTypes.ImageStore, repo string,
	digest godigest.Digest, index ispec.Index, log zlog.Logger,
) (bool, error) {
	for _, desc := range index.Manifests {
		var found bool

		switch desc.MediaType {
		case ispec.MediaTypeImageIndex:
			if digest == desc.Digest {
				// no need to look further if we have a match
				return true, nil
			}

			indexImage, err := GetImageIndex(imgStore, repo, desc.Digest, log)
			if err != nil {
				log.Error().Err(err).Str("repository", repo).Str("digest", desc.Digest.String()).
					Msg("failed to read multiarch(index) image")

				return false, err
			}

			found, _ = IsBlobReferencedInImageIndex(imgStore, repo, digest, indexImage, log)
		case ispec.MediaTypeImageManifest:
			found, _ = isBlobReferencedInImageManifest(imgStore, repo, digest, desc.Digest, log)
		default:
			// should return true for digests found in index.json even if we don't know it's mediatype
			if digest == desc.Digest {
				log.Debug().Str("mediatype", desc.MediaType).Str("digest", digest.String()).
					Msg("unexpected media-type found in image index manifest list")
				found = true
			}
		}

		if found {
			return true, nil
		}
	}

	return false, nil
}

func IsBlobReferenced(imgStore storageTypes.ImageStore, repo string,
	digest godigest.Digest, log zlog.Logger,
) (bool, error) {
	dir := path.Join(imgStore.RootDir(), repo)
	if !imgStore.DirExists(dir) {
		return false, zerr.ErrRepoNotFound
	}

	index, err := GetIndex(imgStore, repo, log)
	if err != nil {
		return false, err
	}

	return IsBlobReferencedInImageIndex(imgStore, repo, digest, index, log)
}

func ApplyLinter(imgStore storageTypes.ImageStore, linter Lint, repo string, descriptor ispec.Descriptor,
) (bool, error) {
	pass := true

	// we'll skip anything that's not a image manifest
	if descriptor.MediaType != ispec.MediaTypeImageManifest {
		return pass, nil
	}

	if linter != nil && !IsSignature(descriptor) {
		// lint new index with new manifest before writing to disk
		pass, err := linter.Lint(repo, descriptor.Digest, imgStore)
		if err != nil {
			return false, err
		}

		if !pass {
			return false, zerr.ErrImageLintAnnotations
		}
	}

	return pass, nil
}

func IsSignature(descriptor ispec.Descriptor) bool {
	tag := descriptor.Annotations[ispec.AnnotationRefName]

	switch descriptor.MediaType {
	case ispec.MediaTypeImageManifest:
		// is cosgin signature
		if strings.HasPrefix(tag, "sha256-") && strings.HasSuffix(tag, cosignSignatureTagSuffix) {
			return true
		}

		// is cosign signature (OCI 1.1 support)
		if descriptor.ArtifactType == zcommon.ArtifactTypeCosign {
			return true
		}

		// is notation signature
		if descriptor.ArtifactType == zcommon.ArtifactTypeNotation {
			return true
		}
	default:
		return false
	}

	return false
}

func GetReferrers(imgStore storageTypes.ImageStore, repo string, gdigest godigest.Digest, artifactTypes []string,
	log zlog.Logger,
) (ispec.Index, error) {
	nilIndex := ispec.Index{}

	if err := gdigest.Validate(); err != nil {
		return nilIndex, err
	}

	dir := path.Join(imgStore.RootDir(), repo)
	if !imgStore.DirExists(dir) {
		return nilIndex, zerr.ErrRepoNotFound
	}

	index, err := GetIndex(imgStore, repo, log)
	if err != nil {
		return nilIndex, err
	}

	result := []ispec.Descriptor{}

	for _, descriptor := range index.Manifests {
		if descriptor.Digest == gdigest {
			continue
		}

		buf, err := imgStore.GetBlobContent(repo, descriptor.Digest)
		if err != nil {
			log.Error().Err(err).Str("blob", imgStore.BlobPath(repo, descriptor.Digest)).Msg("failed to read manifest")

			if errors.Is(err, zerr.ErrBlobNotFound) {
				return nilIndex, zerr.ErrManifestNotFound
			}

			return nilIndex, err
		}

		switch descriptor.MediaType {
		case ispec.MediaTypeImageManifest:
			var manifestContent ispec.Manifest

			if err := json.Unmarshal(buf, &manifestContent); err != nil {
				log.Error().Err(err).Str("manifest digest", descriptor.Digest.String()).Msg("invalid JSON")

				return nilIndex, err
			}

			if manifestContent.Subject == nil || manifestContent.Subject.Digest != gdigest {
				continue
			}

			// filter by artifact type
			manifestArtifactType := zcommon.GetManifestArtifactType(manifestContent)

			if len(artifactTypes) > 0 && !zcommon.Contains(artifactTypes, manifestArtifactType) {
				continue
			}

			result = append(result, ispec.Descriptor{
				MediaType:    descriptor.MediaType,
				ArtifactType: manifestArtifactType,
				Size:         descriptor.Size,
				Digest:       descriptor.Digest,
				Annotations:  manifestContent.Annotations,
			})
		case ispec.MediaTypeImageIndex:
			var indexContent ispec.Index

			if err := json.Unmarshal(buf, &indexContent); err != nil {
				log.Error().Err(err).Str("manifest digest", descriptor.Digest.String()).Msg("invalid JSON")

				return nilIndex, err
			}

			if indexContent.Subject == nil || indexContent.Subject.Digest != gdigest {
				continue
			}

			indexArtifactType := zcommon.GetIndexArtifactType(indexContent)

			if len(artifactTypes) > 0 && !zcommon.Contains(artifactTypes, indexArtifactType) {
				continue
			}

			result = append(result, ispec.Descriptor{
				MediaType:    descriptor.MediaType,
				ArtifactType: indexArtifactType,
				Size:         descriptor.Size,
				Digest:       descriptor.Digest,
				Annotations:  indexContent.Annotations,
			})
		}
	}

	index = ispec.Index{
		Versioned:   imeta.Versioned{SchemaVersion: storageConstants.SchemaVersion},
		MediaType:   ispec.MediaTypeImageIndex,
		Manifests:   result,
		Annotations: map[string]string{},
	}

	return index, nil
}

// Get blob descriptor from it's manifest contents, if blob can not be found it will return error.
func GetBlobDescriptorFromRepo(imgStore storageTypes.ImageStore, repo string, blobDigest godigest.Digest,
	log zlog.Logger,
) (ispec.Descriptor, error) {
	index, err := GetIndex(imgStore, repo, log)
	if err != nil {
		return ispec.Descriptor{}, err
	}

	return GetBlobDescriptorFromIndex(imgStore, index, repo, blobDigest, log)
}

func GetBlobDescriptorFromIndex(imgStore storageTypes.ImageStore, index ispec.Index, repo string,
	blobDigest godigest.Digest, log zlog.Logger,
) (ispec.Descriptor, error) {
	for _, desc := range index.Manifests {
		if desc.Digest == blobDigest {
			return desc, nil
		}

		switch desc.MediaType {
		case ispec.MediaTypeImageManifest:
			if foundDescriptor, err := getBlobDescriptorFromManifest(imgStore, repo, blobDigest, desc, log); err == nil {
				return foundDescriptor, nil
			}
		case ispec.MediaTypeImageIndex:
			indexImage, err := GetImageIndex(imgStore, repo, desc.Digest, log)
			if err != nil {
				return ispec.Descriptor{}, err
			}

			if foundDescriptor, err := GetBlobDescriptorFromIndex(imgStore, indexImage, repo, blobDigest, log); err == nil {
				return foundDescriptor, nil
			}
		}
	}

	return ispec.Descriptor{}, zerr.ErrBlobNotFound
}

func getBlobDescriptorFromManifest(imgStore storageTypes.ImageStore, repo string, blobDigest godigest.Digest,
	desc ispec.Descriptor, log zlog.Logger,
) (ispec.Descriptor, error) {
	manifest, err := GetImageManifest(imgStore, repo, desc.Digest, log)
	if err != nil {
		return ispec.Descriptor{}, err
	}

	if manifest.Config.Digest == blobDigest {
		return manifest.Config, nil
	}

	for _, layer := range manifest.Layers {
		if layer.Digest == blobDigest {
			return layer, nil
		}
	}

	return ispec.Descriptor{}, zerr.ErrBlobNotFound
}

func IsSupportedMediaType(compats []compat.MediaCompatibility, mediaType string) bool {
	// check for some supported legacy formats if configured
	for _, comp := range compats {
		if comp == compat.DockerManifestV2SchemaV2 &&
			(mediaType == docker.MediaTypeManifest || mediaType == dockerList.MediaTypeManifestList) {
			return true
		}
	}

	return mediaType == ispec.MediaTypeImageIndex ||
		mediaType == ispec.MediaTypeImageManifest
}

func IsNonDistributable(mediaType string) bool {
	return mediaType == ispec.MediaTypeImageLayerNonDistributable || //nolint:staticcheck
		mediaType == ispec.MediaTypeImageLayerNonDistributableGzip || //nolint:staticcheck
		mediaType == ispec.MediaTypeImageLayerNonDistributableZstd //nolint:staticcheck
}

func ValidateManifestSchema(buf []byte) error {
	if err := schema.ValidatorMediaTypeManifest.Validate(bytes.NewBuffer(buf)); err != nil {
		if !IsEmptyLayersError(err) {
			return err
		}
	}

	return nil
}

func ValidateImageIndexSchema(buf []byte) error {
	if err := schema.ValidatorMediaTypeImageIndex.Validate(bytes.NewBuffer(buf)); err != nil {
		return err
	}

	return nil
}

func IsEmptyLayersError(err error) bool {
	var validationErr *jsonschema.ValidationError
	if errors.As(err, &validationErr) {
		if len(validationErr.Causes) == 1 && strings.Contains(err.Error(), manifestWithEmptyLayersErrMsg) {
			return true
		} else {
			return false
		}
	}

	return false
}

/*
	DedupeTaskGenerator takes all blobs paths found in the storage.imagestore and groups them by digest

for each digest and based on the dedupe value it will dedupe or restore deduped blobs to the original state(undeduped)\
by creating a task for each digest and pushing it to the task scheduler.
*/
type DedupeTaskGenerator struct {
	ImgStore storageTypes.ImageStore
	// storage dedupe value
	Dedupe bool
	// store blobs paths grouped by digest
	digest         godigest.Digest
	duplicateBlobs []string
	/* store processed digest, used for iterating duplicateBlobs one by one
	and generating a task for each unprocessed one*/
	lastDigests []godigest.Digest
	done        bool
	repos       []string // list of repos on which we run dedupe
	Log         zlog.Logger
}

func (gen *DedupeTaskGenerator) Name() string {
	return "DedupeTaskGenerator"
}

func (gen *DedupeTaskGenerator) Next() (scheduler.Task, error) {
	var err error

	/* at first run get from storage currently found repositories so that we skip the ones that gets synced/uploaded
	while this generator runs, there are deduped/restored inline, no need to run dedupe/restore again */
	if len(gen.repos) == 0 {
		gen.repos, err = gen.ImgStore.GetRepositories()
		if err != nil {
			//nolint: dupword
			gen.Log.Error().Err(err).Str("component", "dedupe").Msg("failed to get list of repositories")

			return nil, err
		}

		// if still no repos
		if len(gen.repos) == 0 {
			gen.Log.Info().Str("component", "dedupe").Msg("no repositories found in storage, finished.")

			// no repositories in storage, no need to continue
			gen.done = true

			return nil, nil //nolint:nilnil
		}
	}

	// get all blobs from storage.imageStore and group them by digest
	gen.digest, gen.duplicateBlobs, err = gen.ImgStore.GetNextDigestWithBlobPaths(gen.repos, gen.lastDigests)
	if err != nil {
		gen.Log.Error().Err(err).Str("component", "dedupe").Msg("failed to get next digest")

		return nil, err
	}

	// if no digests left, then mark the task generator as done
	if gen.digest == "" {
		gen.Log.Info().Str("component", "dedupe").Msg("no digests left, finished")

		gen.done = true

		return nil, nil //nolint:nilnil
	}

	// mark digest as processed before running its task
	gen.lastDigests = append(gen.lastDigests, gen.digest)

	// generate rebuild dedupe task for this digest
	return newDedupeTask(gen.ImgStore, gen.digest, gen.Dedupe, gen.duplicateBlobs, gen.Log), nil
}

func (gen *DedupeTaskGenerator) IsDone() bool {
	return gen.done
}

func (gen *DedupeTaskGenerator) IsReady() bool {
	return true
}

func (gen *DedupeTaskGenerator) Reset() {
	gen.lastDigests = []godigest.Digest{}
	gen.duplicateBlobs = []string{}
	gen.repos = []string{}
	gen.digest = ""
	gen.done = false
}

type dedupeTask struct {
	imgStore storageTypes.ImageStore
	// digest of duplicateBLobs
	digest godigest.Digest
	// blobs paths with the same digest ^
	duplicateBlobs []string
	dedupe         bool
	log            zlog.Logger
}

func newDedupeTask(imgStore storageTypes.ImageStore, digest godigest.Digest, dedupe bool,
	duplicateBlobs []string, log zlog.Logger,
) *dedupeTask {
	return &dedupeTask{imgStore, digest, duplicateBlobs, dedupe, log}
}

func (dt *dedupeTask) DoWork(ctx context.Context) error {
	// run task
	err := dt.imgStore.RunDedupeForDigest(ctx, dt.digest, dt.dedupe, dt.duplicateBlobs) //nolint: contextcheck
	if err != nil {
		// log it
		dt.log.Error().Err(err).Str("digest", dt.digest.String()).Str("component", "dedupe").
			Msg("failed to rebuild digest")
	}

	return err
}

func (dt *dedupeTask) String() string {
	return fmt.Sprintf("{Name: %s, digest: %s, dedupe: %t}",
		dt.Name(), dt.digest, dt.dedupe)
}

func (dt *dedupeTask) Name() string {
	return "DedupeTask"
}

func NewStorageMetricsInitGenerator(imgStore storageTypes.ImageStore, metrics monitoring.MetricServer, log zlog.Logger,
) *StorageMetricsInitGenerator {
	processedRepos := make(map[string]struct{})

	return &StorageMetricsInitGenerator{
		ImgStore:       imgStore,
		Metrics:        metrics,
		Log:            log,
		processedRepos: processedRepos,
		MaxDelay:       15, //nolint:mnd
	}
}

type StorageMetricsInitGenerator struct {
	ImgStore       storageTypes.ImageStore
	done           bool
	Metrics        monitoring.MetricServer
	processedRepos map[string]struct{}
	nextRun        time.Time
	rand           *rand.Rand
	Log            zlog.Logger
	MaxDelay       int
}

func (gen *StorageMetricsInitGenerator) Name() string {
	return "StorageMetricsInitGenerator"
}

func (gen *StorageMetricsInitGenerator) Next() (scheduler.Task, error) {
	if len(gen.processedRepos) == 0 && gen.nextRun.IsZero() {
		gen.rand = rand.New(rand.NewSource(time.Now().UTC().UnixNano())) //nolint: gosec
	}

	delay := gen.rand.Intn(gen.MaxDelay)

	gen.nextRun = time.Now().Add(time.Duration(delay) * time.Second)

	repo, err := gen.ImgStore.GetNextRepository(gen.processedRepos)
	if err != nil {
		return nil, err
	}

	gen.Log.Debug().Str("repo", repo).Int("randomDelay", delay).Msg("generate task for storage metrics")

	if repo == "" {
		gen.done = true

		return nil, nil //nolint:nilnil
	}

	gen.processedRepos[repo] = struct{}{}

	return NewStorageMetricsTask(gen.ImgStore, gen.Metrics, repo, gen.Log), nil
}

func (gen *StorageMetricsInitGenerator) IsDone() bool {
	return gen.done
}

func (gen *StorageMetricsInitGenerator) IsReady() bool {
	return time.Now().After(gen.nextRun)
}

func (gen *StorageMetricsInitGenerator) Reset() {
	gen.processedRepos = make(map[string]struct{})
	gen.done = false
	gen.nextRun = time.Time{}
}

type smTask struct {
	imgStore storageTypes.ImageStore
	metrics  monitoring.MetricServer
	repo     string
	log      zlog.Logger
}

func NewStorageMetricsTask(imgStore storageTypes.ImageStore, metrics monitoring.MetricServer, repo string,
	log zlog.Logger,
) *smTask {
	return &smTask{imgStore, metrics, repo, log}
}

func (smt *smTask) DoWork(ctx context.Context) error {
	// run task
	monitoring.SetStorageUsage(smt.metrics, smt.imgStore.RootDir(), smt.repo)
	smt.log.Debug().Str("component", "monitoring").Msg("computed storage usage for repo " + smt.repo)

	return nil
}

func (smt *smTask) String() string {
	return fmt.Sprintf("{Name: \"%s\", repo: \"%s\"}",
		smt.Name(), smt.repo)
}

func (smt *smTask) Name() string {
	return "StorageMetricsTask"
}
