//go:build sync
// +build sync

package sync

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/containers/image/v5/copy"
	"github.com/containers/image/v5/docker"
	"github.com/containers/image/v5/manifest"
	"github.com/containers/image/v5/pkg/blobinfocache/none"
	"github.com/containers/image/v5/signature"
	"github.com/containers/image/v5/types"
	"github.com/docker/distribution/reference"
	"github.com/opencontainers/go-digest"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"

	zerr "zotregistry.io/zot/errors"
	"zotregistry.io/zot/pkg/common"
	syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
	"zotregistry.io/zot/pkg/log"
	"zotregistry.io/zot/pkg/test/inject"
)

const (
	SyncBlobUploadDir = ".sync"
)

// Get sync.FileCredentials from file.
func getFileCredentials(filepath string) (syncconf.CredentialsFile, error) {
	credsFile, err := os.ReadFile(filepath)
	if err != nil {
		return nil, err
	}

	var creds syncconf.CredentialsFile

	err = json.Unmarshal(credsFile, &creds)
	if err != nil {
		return nil, err
	}

	return creds, nil
}

func getUpstreamContext(certDir, username, password string, tlsVerify bool) *types.SystemContext {
	upstreamCtx := &types.SystemContext{}
	upstreamCtx.DockerCertPath = certDir
	upstreamCtx.DockerDaemonCertPath = certDir

	if tlsVerify {
		upstreamCtx.DockerDaemonInsecureSkipTLSVerify = false
		upstreamCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(false)
	} else {
		upstreamCtx.DockerDaemonInsecureSkipTLSVerify = true
		upstreamCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
	}

	if username != "" && password != "" {
		upstreamCtx.DockerAuthConfig = &types.DockerAuthConfig{
			Username: username,
			Password: password,
		}
	}

	return upstreamCtx
}

// sync needs transport to be stripped to not be wrongly interpreted as an image reference
// at a non-fully qualified registry (hostname as image and port as tag).
func StripRegistryTransport(url string) string {
	return strings.Replace(strings.Replace(url, "http://", "", 1), "https://", "", 1)
}

// getRepoTags lists all tags in a repository.
// It returns a string slice of tags and any error encountered.
func getRepoTags(ctx context.Context, sysCtx *types.SystemContext, host, repo string) ([]string, error) {
	repoRef, err := parseRepositoryReference(fmt.Sprintf("%s/%s", host, repo))
	if err != nil {
		return []string{}, err
	}

	dockerRef, err := docker.NewReference(reference.TagNameOnly(repoRef))
	// hard to reach test case, injected error, see pkg/test/dev.go
	if err = inject.Error(err); err != nil {
		return nil, err // Should never happen for a reference with tag and no digest
	}

	tags, err := docker.GetRepositoryTags(ctx, sysCtx, dockerRef)
	if err != nil {
		return nil, err
	}

	return tags, nil
}

// parseRepositoryReference parses input into a reference.Named, and verifies that it names a repository, not an image.
func parseRepositoryReference(input string) (reference.Named, error) {
	ref, err := reference.ParseNormalizedNamed(input)
	if err != nil {
		return nil, err
	}

	if !reference.IsNameOnly(ref) {
		return nil, zerr.ErrInvalidRepositoryName
	}

	return ref, nil
}

// parse a reference, return its digest and if it's valid.
func parseReference(reference string) (digest.Digest, bool) {
	var ok bool

	d, err := digest.Parse(reference)
	if err == nil {
		ok = true
	}

	return d, ok
}

func getCopyOptions(upstreamCtx, localCtx *types.SystemContext) copy.Options {
	options := copy.Options{
		DestinationCtx:        localCtx,
		SourceCtx:             upstreamCtx,
		ReportWriter:          io.Discard,
		ForceManifestMIMEType: ispec.MediaTypeImageManifest, // force only oci manifest MIME type
		ImageListSelection:    copy.CopyAllImages,
	}

	return options
}

func getPolicyContext(log log.Logger) (*signature.PolicyContext, error) {
	policy := &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}

	policyContext, err := signature.NewPolicyContext(policy)
	if err := inject.Error(err); err != nil {
		log.Error().Str("errorType", common.TypeOf(err)).
			Err(err).Msg("couldn't create policy context")

		return nil, err
	}

	return policyContext, nil
}

func getSupportedMediaType() []string {
	return []string{
		ispec.MediaTypeImageIndex,
		ispec.MediaTypeImageManifest,
		manifest.DockerV2ListMediaType,
		manifest.DockerV2Schema2MediaType,
	}
}

func isSupportedMediaType(mediaType string) bool {
	mediaTypes := getSupportedMediaType()
	for _, m := range mediaTypes {
		if m == mediaType {
			return true
		}
	}

	return false
}

// given an imageSource and a docker manifest, convert it to OCI.
func convertDockerManifestToOCI(imageSource types.ImageSource, dockerManifestBuf []byte) ([]byte, error) {
	var ociManifest ispec.Manifest

	// unmarshal docker manifest into OCI manifest
	err := json.Unmarshal(dockerManifestBuf, &ociManifest)
	if err != nil {
		return []byte{}, err
	}

	configContent, err := getImageConfigContent(imageSource, ociManifest.Config.Digest)
	if err != nil {
		return []byte{}, err
	}

	// marshal config blob into OCI config, will remove keys specific to docker
	var ociConfig ispec.Image

	err = json.Unmarshal(configContent, &ociConfig)
	if err != nil {
		return []byte{}, err
	}

	ociConfigContent, err := json.Marshal(ociConfig)
	if err != nil {
		return []byte{}, err
	}

	// convert layers
	err = convertDockerLayersToOCI(ociManifest.Layers)
	if err != nil {
		return []byte{}, err
	}

	// convert config and manifest mediatype
	ociManifest.Config.Size = int64(len(ociConfigContent))
	ociManifest.Config.Digest = digest.FromBytes(ociConfigContent)
	ociManifest.Config.MediaType = ispec.MediaTypeImageConfig
	ociManifest.MediaType = ispec.MediaTypeImageManifest

	return json.Marshal(ociManifest)
}

// convert docker layers mediatypes to OCI mediatypes.
func convertDockerLayersToOCI(dockerLayers []ispec.Descriptor) error {
	for idx, layer := range dockerLayers {
		switch layer.MediaType {
		case manifest.DockerV2Schema2ForeignLayerMediaType:
			dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerNonDistributable //nolint: staticcheck
		case manifest.DockerV2Schema2ForeignLayerMediaTypeGzip:
			dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerNonDistributableGzip //nolint: staticcheck
		case manifest.DockerV2SchemaLayerMediaTypeUncompressed:
			dockerLayers[idx].MediaType = ispec.MediaTypeImageLayer
		case manifest.DockerV2Schema2LayerMediaType:
			dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerGzip
		default:
			return zerr.ErrMediaTypeNotSupported
		}
	}

	return nil
}

// given an imageSource and a docker index manifest, convert it to OCI.
func convertDockerIndexToOCI(imageSource types.ImageSource, dockerManifestBuf []byte) ([]byte, error) {
	// get docker index
	originalIndex, err := manifest.ListFromBlob(dockerManifestBuf, manifest.DockerV2ListMediaType)
	if err != nil {
		return []byte{}, err
	}

	// get manifests digests
	manifestsDigests := originalIndex.Instances()

	manifestsUpdates := make([]manifest.ListUpdate, 0, len(manifestsDigests))

	// convert each manifests in index from docker to OCI
	for _, manifestDigest := range manifestsDigests {
		digestCopy := manifestDigest

		indexManifestBuf, _, err := imageSource.GetManifest(context.Background(), &digestCopy)
		if err != nil {
			return []byte{}, err
		}

		convertedIndexManifest, err := convertDockerManifestToOCI(imageSource, indexManifestBuf)
		if err != nil {
			return []byte{}, err
		}

		manifestsUpdates = append(manifestsUpdates, manifest.ListUpdate{
			Digest:    digest.FromBytes(convertedIndexManifest),
			Size:      int64(len(convertedIndexManifest)),
			MediaType: ispec.MediaTypeImageManifest,
		})
	}

	// update all manifests in index
	if err := originalIndex.UpdateInstances(manifestsUpdates); err != nil {
		return []byte{}, err
	}

	// convert index to OCI
	convertedList, err := originalIndex.ConvertToMIMEType(ispec.MediaTypeImageIndex)
	if err != nil {
		return []byte{}, err
	}

	return convertedList.Serialize()
}

// given an image source and a config blob digest, get blob config content.
func getImageConfigContent(imageSource types.ImageSource, configDigest digest.Digest,
) ([]byte, error) {
	configBlob, _, err := imageSource.GetBlob(context.Background(), types.BlobInfo{
		Digest: configDigest,
	}, none.NoCache)
	if err != nil {
		return nil, err
	}

	configBuf := new(bytes.Buffer)

	_, err = configBuf.ReadFrom(configBlob)

	return configBuf.Bytes(), err
}
