//go:build sync
// +build sync

package references

import (
	"errors"
	"fmt"
	"net/http"
	"strings"

	godigest "github.com/opencontainers/go-digest"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/sigstore/cosign/v2/pkg/oci/remote"

	zerr "zotregistry.io/zot/errors"
	"zotregistry.io/zot/pkg/common"
	"zotregistry.io/zot/pkg/extensions/sync/constants"
	client "zotregistry.io/zot/pkg/extensions/sync/httpclient"
	"zotregistry.io/zot/pkg/log"
	"zotregistry.io/zot/pkg/meta"
	mTypes "zotregistry.io/zot/pkg/meta/types"
	"zotregistry.io/zot/pkg/storage"
)

type CosignReference struct {
	client          *client.Client
	storeController storage.StoreController
	metaDB          mTypes.MetaDB
	log             log.Logger
}

func NewCosignReference(httpClient *client.Client, storeController storage.StoreController,
	metaDB mTypes.MetaDB, log log.Logger,
) CosignReference {
	return CosignReference{
		client:          httpClient,
		storeController: storeController,
		metaDB:          metaDB,
		log:             log,
	}
}

func (ref CosignReference) Name() string {
	return constants.Cosign
}

func (ref CosignReference) IsSigned(upstreamRepo, subjectDigestStr string) bool {
	cosignSignatureTag := getCosignSignatureTagFromSubjectDigest(subjectDigestStr)
	_, _, err := ref.getManifest(upstreamRepo, cosignSignatureTag)

	return err == nil
}

func (ref CosignReference) canSkipReferences(localRepo, digest string, manifest *ispec.Manifest) (
	bool, error,
) {
	if manifest == nil {
		return true, nil
	}

	imageStore := ref.storeController.GetImageStore(localRepo)

	// check cosign signature already synced
	_, localDigest, _, err := imageStore.GetImageManifest(localRepo, digest)
	if err != nil {
		if errors.Is(err, zerr.ErrManifestNotFound) {
			return false, nil
		}

		ref.log.Error().Str("errorType", common.TypeOf(err)).Err(err).
			Str("repository", localRepo).Str("reference", digest).
			Msg("couldn't get local cosign manifest")

		return false, err
	}

	if localDigest.String() != digest {
		return false, nil
	}

	ref.log.Info().Str("repository", localRepo).Str("reference", digest).
		Msg("skipping syncing cosign reference, already synced")

	return true, nil
}

func (ref CosignReference) SyncReferences(localRepo, remoteRepo, subjectDigestStr string) ([]godigest.Digest, error) {
	cosignTags := getCosignTagsFromSubjectDigest(subjectDigestStr)

	refsDigests := make([]godigest.Digest, 0, len(cosignTags))

	for _, cosignTag := range cosignTags {
		manifest, manifestBuf, err := ref.getManifest(remoteRepo, cosignTag)
		if err != nil {
			if errors.Is(err, zerr.ErrSyncReferrerNotFound) {
				continue
			}

			return refsDigests, err
		}

		digest := godigest.FromBytes(manifestBuf)

		skip, err := ref.canSkipReferences(localRepo, digest.String(), manifest)
		if err != nil {
			ref.log.Error().Err(err).Str("repository", localRepo).Str("subject", subjectDigestStr).
				Msg("couldn't check if the remote image cosign reference can be skipped")
		}

		if skip {
			refsDigests = append(refsDigests, digest)

			continue
		}

		imageStore := ref.storeController.GetImageStore(localRepo)

		ref.log.Info().Str("repository", localRepo).Str("subject", subjectDigestStr).
			Msg("syncing cosign reference for image")

		for _, blob := range manifest.Layers {
			if err := syncBlob(ref.client, imageStore, localRepo, remoteRepo, blob.Digest, ref.log); err != nil {
				return refsDigests, err
			}
		}

		// sync config blob
		if err := syncBlob(ref.client, imageStore, localRepo, remoteRepo, manifest.Config.Digest, ref.log); err != nil {
			return refsDigests, err
		}

		// push manifest
		referenceDigest, _, err := imageStore.PutImageManifest(localRepo, cosignTag,
			ispec.MediaTypeImageManifest, manifestBuf)
		if err != nil {
			ref.log.Error().Str("errorType", common.TypeOf(err)).
				Str("repository", localRepo).Str("subject", subjectDigestStr).
				Err(err).Msg("couldn't upload cosign reference manifest for image")

			return refsDigests, err
		}

		refsDigests = append(refsDigests, digest)

		ref.log.Info().Str("repository", localRepo).Str("subject", subjectDigestStr).
			Msg("successfully synced cosign reference for image")

		if ref.metaDB != nil {
			ref.log.Debug().Str("repository", localRepo).Str("subject", subjectDigestStr).
				Msg("metaDB: trying to sync cosign reference for image")

			isSig, sigType, signedManifestDig, err := storage.CheckIsImageSignature(localRepo, manifestBuf,
				cosignTag)
			if err != nil {
				return refsDigests, fmt.Errorf("failed to check if cosign reference '%s@%s' is a signature: %w", localRepo,
					cosignTag, err)
			}

			if isSig {
				err = ref.metaDB.AddManifestSignature(localRepo, signedManifestDig, mTypes.SignatureMetadata{
					SignatureType:   sigType,
					SignatureDigest: referenceDigest.String(),
				})
			} else {
				err = meta.SetImageMetaFromInput(localRepo, cosignTag, ispec.MediaTypeImageManifest,
					referenceDigest, manifestBuf, ref.storeController.GetImageStore(localRepo),
					ref.metaDB, ref.log)
			}

			if err != nil {
				return refsDigests, fmt.Errorf("failed to set metadata for cosign reference in '%s@%s': %w",
					localRepo, subjectDigestStr, err)
			}

			ref.log.Info().Str("repository", localRepo).Str("subject", subjectDigestStr).
				Msg("metaDB: successfully added cosign reference for image")
		}
	}

	return refsDigests, nil
}

func (ref CosignReference) getManifest(repo, cosignTag string) (*ispec.Manifest, []byte, error) {
	var cosignManifest ispec.Manifest

	body, _, statusCode, err := ref.client.MakeGetRequest(&cosignManifest, ispec.MediaTypeImageManifest,
		"v2", repo, "manifests", cosignTag)
	if err != nil {
		if statusCode == http.StatusNotFound {
			ref.log.Debug().Str("errorType", common.TypeOf(err)).
				Str("repository", repo).Str("tag", cosignTag).
				Err(err).Msg("couldn't find any cosign manifest for image")

			return nil, nil, zerr.ErrSyncReferrerNotFound
		}

		ref.log.Error().Str("errorType", common.TypeOf(err)).
			Str("repository", repo).Str("tag", cosignTag).Int("statusCode", statusCode).
			Err(err).Msg("couldn't get cosign manifest for image")

		return nil, nil, err
	}

	return &cosignManifest, body, nil
}

func getCosignSignatureTagFromSubjectDigest(digestStr string) string {
	return strings.Replace(digestStr, ":", "-", 1) + "." + remote.SignatureTagSuffix
}

func getCosignSBOMTagFromSubjectDigest(digestStr string) string {
	return strings.Replace(digestStr, ":", "-", 1) + "." + remote.SBOMTagSuffix
}

func getCosignTagsFromSubjectDigest(digestStr string) []string {
	var cosignTags []string

	// signature tag
	cosignTags = append(cosignTags, getCosignSignatureTagFromSubjectDigest(digestStr))
	// sbom tag
	cosignTags = append(cosignTags, getCosignSBOMTagFromSubjectDigest(digestStr))

	return cosignTags
}

// this function will check if tag is a cosign tag (signature or sbom).
func IsCosignTag(tag string) bool {
	if strings.HasPrefix(tag, "sha256-") &&
		(strings.HasSuffix(tag, remote.SignatureTagSuffix) || strings.HasSuffix(tag, remote.SBOMTagSuffix)) {
		return true
	}

	return false
}
