package registryclient

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"

	"github.com/distribution/reference"
	"github.com/docker/cli/cli/manifest/types"
	"github.com/docker/cli/internal/registry"
	"github.com/docker/distribution"
	"github.com/docker/distribution/manifest/manifestlist"
	"github.com/docker/distribution/manifest/ocischema"
	"github.com/docker/distribution/manifest/schema2"
	"github.com/docker/distribution/registry/api/errcode"
	v2 "github.com/docker/distribution/registry/api/v2"
	distclient "github.com/docker/distribution/registry/client"
	"github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/sirupsen/logrus"
)

// fetchManifest pulls a manifest from a registry and returns it. An error
// is returned if no manifest is found matching namedRef.
func fetchManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (types.ImageManifest, error) {
	manifest, err := getManifest(ctx, repo, ref)
	if err != nil {
		return types.ImageManifest{}, err
	}

	switch v := manifest.(type) {
	// Removed Schema 1 support
	case *schema2.DeserializedManifest:
		return pullManifestSchemaV2(ctx, ref, repo, *v)
	case *ocischema.DeserializedManifest:
		return pullManifestOCISchema(ctx, ref, repo, *v)
	case *manifestlist.DeserializedManifestList:
		return types.ImageManifest{}, fmt.Errorf("%s is a manifest list", ref)
	}
	return types.ImageManifest{}, fmt.Errorf("%s is not a manifest", ref)
}

func fetchList(ctx context.Context, repo distribution.Repository, ref reference.Named) ([]types.ImageManifest, error) {
	manifest, err := getManifest(ctx, repo, ref)
	if err != nil {
		return nil, err
	}

	switch v := manifest.(type) {
	case *manifestlist.DeserializedManifestList:
		return pullManifestList(ctx, ref, repo, *v)
	default:
		return nil, fmt.Errorf("unsupported manifest format: %v", v)
	}
}

func getManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (distribution.Manifest, error) {
	manSvc, err := repo.Manifests(ctx)
	if err != nil {
		return nil, err
	}

	dgst, opts, err := getManifestOptionsFromReference(ref)
	if err != nil {
		return nil, fmt.Errorf("image manifest for %q does not exist", ref)
	}
	return manSvc.Get(ctx, dgst, opts...)
}

func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst schema2.DeserializedManifest) (types.ImageManifest, error) {
	manifestDesc, err := validateManifestDigest(ref, mfst)
	if err != nil {
		return types.ImageManifest{}, err
	}
	configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo)
	if err != nil {
		return types.ImageManifest{}, err
	}

	if manifestDesc.Platform == nil {
		manifestDesc.Platform = &ocispec.Platform{}
	}

	// Fill in os and architecture fields from config JSON
	if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil {
		return types.ImageManifest{}, err
	}

	return types.NewImageManifest(ref, manifestDesc, &mfst), nil
}

func pullManifestOCISchema(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst ocischema.DeserializedManifest) (types.ImageManifest, error) {
	manifestDesc, err := validateManifestDigest(ref, mfst)
	if err != nil {
		return types.ImageManifest{}, err
	}
	configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo)
	if err != nil {
		return types.ImageManifest{}, err
	}

	if manifestDesc.Platform == nil {
		manifestDesc.Platform = &ocispec.Platform{}
	}

	// Fill in os and architecture fields from config JSON
	if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil {
		return types.ImageManifest{}, err
	}

	return types.NewOCIImageManifest(ref, manifestDesc, &mfst), nil
}

func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) {
	blobs := repo.Blobs(ctx)
	configJSON, err := blobs.Get(ctx, dgst)
	if err != nil {
		return nil, err
	}

	verifier := dgst.Verifier()
	if _, err := verifier.Write(configJSON); err != nil {
		return nil, err
	}
	if !verifier.Verified() {
		return nil, fmt.Errorf("image config verification failed for digest %s", dgst)
	}
	return configJSON, nil
}

// validateManifestDigest computes the manifest digest, and, if pulling by
// digest, ensures that it matches the requested digest.
func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (ocispec.Descriptor, error) {
	mediaType, canonical, err := mfst.Payload()
	if err != nil {
		return ocispec.Descriptor{}, err
	}
	desc := ocispec.Descriptor{
		Digest:    digest.FromBytes(canonical),
		Size:      int64(len(canonical)),
		MediaType: mediaType,
	}

	// If pull by digest, then verify the manifest digest.
	if digested, isDigested := ref.(reference.Canonical); isDigested && digested.Digest() != desc.Digest {
		return ocispec.Descriptor{}, fmt.Errorf("manifest verification failed for digest %s", digested.Digest())
	}

	return desc, nil
}

// pullManifestList handles "manifest lists" which point to various
// platform-specific manifests.
func pullManifestList(ctx context.Context, ref reference.Named, repo distribution.Repository, mfstList manifestlist.DeserializedManifestList) ([]types.ImageManifest, error) {
	if _, err := validateManifestDigest(ref, mfstList); err != nil {
		return nil, err
	}

	infos := make([]types.ImageManifest, 0, len(mfstList.Manifests))
	for _, manifestDescriptor := range mfstList.Manifests {
		manSvc, err := repo.Manifests(ctx)
		if err != nil {
			return nil, err
		}
		manifest, err := manSvc.Get(ctx, manifestDescriptor.Digest)
		if err != nil {
			return nil, err
		}

		manifestRef, err := reference.WithDigest(ref, manifestDescriptor.Digest)
		if err != nil {
			return nil, err
		}

		var imageManifest types.ImageManifest
		switch v := manifest.(type) {
		case *schema2.DeserializedManifest:
			imageManifest, err = pullManifestSchemaV2(ctx, manifestRef, repo, *v)
		case *ocischema.DeserializedManifest:
			imageManifest, err = pullManifestOCISchema(ctx, manifestRef, repo, *v)
		default:
			err = fmt.Errorf("unsupported manifest type: %T", manifest)
		}
		if err != nil {
			return nil, err
		}

		// Replace platform from config
		p := manifestDescriptor.Platform
		imageManifest.Descriptor.Platform = types.OCIPlatform(&p)

		infos = append(infos, imageManifest)
	}
	return infos, nil
}

func continueOnError(err error) bool {
	switch v := err.(type) {
	case errcode.Errors:
		if len(v) == 0 {
			return true
		}
		return continueOnError(v[0])
	case errcode.Error:
		switch e := err.(errcode.Error); e.Code {
		case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
			return true
		default:
			return false
		}
	case *distclient.UnexpectedHTTPResponseError:
		return true
	}
	return false
}

func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error {
	endpoints, err := allEndpoints(ctx, namedRef, c.insecureRegistry)
	if err != nil {
		return err
	}

	repoName := reference.Path(reference.TrimNamed(namedRef))
	indexInfo := registry.NewIndexInfo(namedRef)

	confirmedTLSRegistries := make(map[string]bool)
	for _, endpoint := range endpoints {
		if endpoint.URL.Scheme != "https" {
			if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
				logrus.Debugf("skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
				continue
			}
		}

		if c.insecureRegistry {
			endpoint.TLSConfig.InsecureSkipVerify = true
		}
		repoEndpoint := repositoryEndpoint{
			repoName:  repoName,
			indexInfo: indexInfo,
			endpoint:  endpoint,
		}
		repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint)
		if err != nil {
			logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint)
			var protoErr httpProtoError
			if errors.As(err, &protoErr) {
				continue
			}
			return err
		}

		if endpoint.URL.Scheme == "http" && !c.insecureRegistry {
			logrus.Debugf("skipping non-tls registry endpoint: %s", endpoint.URL)
			continue
		}
		done, err := each(ctx, repo, namedRef)
		if err != nil {
			if continueOnError(err) {
				if endpoint.URL.Scheme == "https" {
					confirmedTLSRegistries[endpoint.URL.Host] = true
				}
				logrus.Debugf("continuing on error (%T) %s", err, err)
				continue
			}
			logrus.Debugf("not continuing on error (%T) %s", err, err)
			return err
		}
		if done {
			return nil
		}
	}
	return notFoundError{errors.New("no such manifest: " + namedRef.String())}
}

// allEndpoints returns a list of endpoints ordered by priority (v2, http).
func allEndpoints(ctx context.Context, namedRef reference.Named, insecure bool) ([]registry.APIEndpoint, error) {
	var serviceOpts registry.ServiceOptions
	if insecure {
		logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef))
		serviceOpts.InsecureRegistries = []string{reference.Domain(namedRef)}
	}
	registryService, err := registry.NewService(serviceOpts)
	if err != nil {
		return nil, err
	}
	endpoints, err := registryService.Endpoints(ctx, reference.Domain(namedRef))
	logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
	return endpoints, err
}

type notFoundError struct{ error }

func (notFoundError) NotFound() {}
