package image

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/containerd/platforms"
	"github.com/distribution/reference"
	"github.com/docker/docker/api"
	"github.com/docker/docker/api/server/httputils"
	"github.com/docker/docker/api/types/backend"
	"github.com/docker/docker/api/types/filters"
	imagetypes "github.com/docker/docker/api/types/image"
	"github.com/docker/docker/api/types/registry"
	"github.com/docker/docker/api/types/versions"
	"github.com/docker/docker/builder/remotecontext"
	"github.com/docker/docker/dockerversion"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/image"
	"github.com/docker/docker/pkg/ioutils"
	"github.com/docker/docker/pkg/progress"
	"github.com/docker/docker/pkg/streamformatter"
	"github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
)

// Creates an image from Pull or from Import
func (ir *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	var (
		img         = r.Form.Get("fromImage")
		repo        = r.Form.Get("repo")
		tag         = r.Form.Get("tag")
		comment     = r.Form.Get("message")
		progressErr error
		output      = ioutils.NewWriteFlusher(w)
		platform    *ocispec.Platform
	)
	defer output.Close()

	w.Header().Set("Content-Type", "application/json")

	version := httputils.VersionFromContext(ctx)
	if versions.GreaterThanOrEqualTo(version, "1.32") {
		if p := r.FormValue("platform"); p != "" {
			sp, err := platforms.Parse(p)
			if err != nil {
				return errdefs.InvalidParameter(err)
			}
			platform = &sp
		}
	}

	if img != "" { // pull
		metaHeaders := map[string][]string{}
		for k, v := range r.Header {
			if strings.HasPrefix(k, "X-Meta-") {
				metaHeaders[k] = v
			}
		}

		// Special case: "pull -a" may send an image name with a
		// trailing :. This is ugly, but let's not break API
		// compatibility.
		imgName := strings.TrimSuffix(img, ":")

		ref, err := reference.ParseNormalizedNamed(imgName)
		if err != nil {
			return errdefs.InvalidParameter(err)
		}

		// TODO(thaJeztah) this could use a WithTagOrDigest() utility
		if tag != "" {
			// The "tag" could actually be a digest.
			var dgst digest.Digest
			dgst, err = digest.Parse(tag)
			if err == nil {
				ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
			} else {
				ref, err = reference.WithTag(ref, tag)
			}
			if err != nil {
				return errdefs.InvalidParameter(err)
			}
		}

		if err := validateRepoName(ref); err != nil {
			return errdefs.Forbidden(err)
		}

		// For a pull it is not an error if no auth was given. Ignore invalid
		// AuthConfig to increase compatibility with the existing API.
		authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
		progressErr = ir.backend.PullImage(ctx, ref, platform, metaHeaders, authConfig, output)
	} else { // import
		src := r.Form.Get("fromSrc")

		tagRef, err := httputils.RepoTagReference(repo, tag)
		if err != nil {
			return errdefs.InvalidParameter(err)
		}

		if comment == "" {
			comment = "Imported from " + src
		}

		var layerReader io.ReadCloser
		defer r.Body.Close()
		if src == "-" {
			layerReader = r.Body
		} else {
			if len(strings.Split(src, "://")) == 1 {
				src = "http://" + src
			}
			u, err := url.Parse(src)
			if err != nil {
				return errdefs.InvalidParameter(err)
			}

			resp, err := remotecontext.GetWithStatusError(u.String())
			if err != nil {
				return err
			}
			output.Write(streamformatter.FormatStatus("", "Downloading from %s", u))
			progressOutput := streamformatter.NewJSONProgressOutput(output, true)
			layerReader = progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Importing")
			defer layerReader.Close()
		}

		var id image.ID
		id, progressErr = ir.backend.ImportImage(ctx, tagRef, platform, comment, layerReader, r.Form["changes"])

		if progressErr == nil {
			_, _ = output.Write(streamformatter.FormatStatus("", "%v", id.String()))
		}
	}
	if progressErr != nil {
		if !output.Flushed() {
			return progressErr
		}
		_, _ = output.Write(streamformatter.FormatError(progressErr))
	}

	return nil
}

func (ir *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	metaHeaders := map[string][]string{}
	for k, v := range r.Header {
		if strings.HasPrefix(k, "X-Meta-") {
			metaHeaders[k] = v
		}
	}
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	var authConfig *registry.AuthConfig
	if authEncoded := r.Header.Get(registry.AuthHeader); authEncoded != "" {
		// the new format is to handle the authConfig as a header. Ignore invalid
		// AuthConfig to increase compatibility with the existing API.
		authConfig, _ = registry.DecodeAuthConfig(authEncoded)
	} else {
		// the old format is supported for compatibility if there was no authConfig header
		var err error
		authConfig, err = registry.DecodeAuthConfigBody(r.Body)
		if err != nil {
			return errors.Wrap(err, "bad parameters and missing X-Registry-Auth")
		}
	}

	output := ioutils.NewWriteFlusher(w)
	defer output.Close()

	w.Header().Set("Content-Type", "application/json")

	img := vars["name"]
	tag := r.Form.Get("tag")

	var ref reference.Named

	// Tag is empty only in case PushOptions.All is true.
	if tag != "" {
		r, err := httputils.RepoTagReference(img, tag)
		if err != nil {
			return errdefs.InvalidParameter(err)
		}
		ref = r
	} else {
		r, err := reference.ParseNormalizedNamed(img)
		if err != nil {
			return errdefs.InvalidParameter(err)
		}
		ref = r
	}

	var platform *ocispec.Platform
	// Platform is optional, and only supported in API version 1.46 and later.
	// However the PushOptions struct previously was an alias for the PullOptions struct
	// which also contained a Platform field.
	// This means that older clients may be sending a platform field, even
	// though it wasn't really supported by the server.
	// Don't break these clients and just ignore the platform field on older APIs.
	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.46") {
		if formPlatform := r.Form.Get("platform"); formPlatform != "" {
			p, err := httputils.DecodePlatform(formPlatform)
			if err != nil {
				return err
			}
			platform = p
		}
	}

	if err := ir.backend.PushImage(ctx, ref, platform, metaHeaders, authConfig, output); err != nil {
		if !output.Flushed() {
			return err
		}
		_, _ = output.Write(streamformatter.FormatError(err))
	}
	return nil
}

func (ir *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	w.Header().Set("Content-Type", "application/x-tar")

	output := ioutils.NewWriteFlusher(w)
	defer output.Close()
	var names []string
	if name, ok := vars["name"]; ok {
		names = []string{name}
	} else {
		names = r.Form["names"]
	}

	var platform *ocispec.Platform
	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
		if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 {
			// TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759
			return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported"))
		}
		if formPlatform := r.Form.Get("platform"); formPlatform != "" {
			p, err := httputils.DecodePlatform(formPlatform)
			if err != nil {
				return err
			}
			platform = p
		}
	}

	if err := ir.backend.ExportImage(ctx, names, platform, output); err != nil {
		if !output.Flushed() {
			return err
		}
		_, _ = output.Write(streamformatter.FormatError(err))
	}
	return nil
}

func (ir *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	var platform *ocispec.Platform
	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
		if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 {
			// TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759
			return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported"))
		}
		if formPlatform := r.Form.Get("platform"); formPlatform != "" {
			p, err := httputils.DecodePlatform(formPlatform)
			if err != nil {
				return err
			}
			platform = p
		}
	}
	quiet := httputils.BoolValueOrDefault(r, "quiet", true)

	w.Header().Set("Content-Type", "application/json")

	output := ioutils.NewWriteFlusher(w)
	defer output.Close()
	if err := ir.backend.LoadImage(ctx, r.Body, platform, output, quiet); err != nil {
		_, _ = output.Write(streamformatter.FormatError(err))
	}
	return nil
}

type missingImageError struct{}

func (missingImageError) Error() string {
	return "image name cannot be blank"
}

func (missingImageError) InvalidParameter() {}

func (ir *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	name := vars["name"]

	if strings.TrimSpace(name) == "" {
		return missingImageError{}
	}

	force := httputils.BoolValue(r, "force")
	prune := !httputils.BoolValue(r, "noprune")

	var platforms []ocispec.Platform
	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.50") {
		p, err := httputils.DecodePlatforms(r.Form["platforms"])
		if err != nil {
			return err
		}
		platforms = p
	}

	list, err := ir.backend.ImageDelete(ctx, name, imagetypes.RemoveOptions{
		Force:         force,
		PruneChildren: prune,
		Platforms:     platforms,
	})
	if err != nil {
		return err
	}

	return httputils.WriteJSON(w, http.StatusOK, list)
}

func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	var manifests bool
	if r.Form.Get("manifests") != "" && versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
		manifests = httputils.BoolValue(r, "manifests")
	}

	var platform *ocispec.Platform
	if r.Form.Get("platform") != "" && versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.49") {
		p, err := httputils.DecodePlatform(r.Form.Get("platform"))
		if err != nil {
			return errdefs.InvalidParameter(err)
		}
		platform = p
	}

	if manifests && platform != nil {
		return errdefs.InvalidParameter(errors.New("conflicting options: manifests and platform options cannot both be set"))
	}

	resp, err := ir.backend.ImageInspect(ctx, vars["name"], backend.ImageInspectOpts{
		Manifests: manifests,
		Platform:  platform,
	})
	if err != nil {
		return err
	}

	// inspectResponse preserves fields in the response that have an
	// "omitempty" in the OCI spec, but didn't omit such fields in
	// legacy responses before API v1.50.
	imageInspect := &inspectCompatResponse{
		InspectResponse: resp,
		legacyConfig:    legacyConfigFields["current"],
	}

	// Make sure we output empty arrays instead of nil. While Go nil slice is functionally equivalent to an empty slice,
	// it matters for the JSON representation.
	if imageInspect.RepoTags == nil {
		imageInspect.RepoTags = []string{}
	}
	if imageInspect.RepoDigests == nil {
		imageInspect.RepoDigests = []string{}
	}

	version := httputils.VersionFromContext(ctx)
	if versions.LessThan(version, "1.44") {
		imageInspect.VirtualSize = imageInspect.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.

		if imageInspect.Created == "" {
			// backwards compatibility for Created not existing returning "0001-01-01T00:00:00Z"
			// https://github.com/moby/moby/issues/47368
			imageInspect.Created = time.Time{}.Format(time.RFC3339Nano)
		}
	}
	if versions.GreaterThanOrEqualTo(version, "1.45") {
		imageInspect.Container = ""        //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
		imageInspect.ContainerConfig = nil //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
	}
	if versions.LessThan(version, "1.48") {
		imageInspect.Descriptor = nil
	}
	if versions.LessThan(version, "1.50") {
		imageInspect.legacyConfig = legacyConfigFields["v1.49"]
	}

	return httputils.WriteJSON(w, http.StatusOK, imageInspect)
}

func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	imageFilters, err := filters.FromJSON(r.Form.Get("filters"))
	if err != nil {
		return err
	}

	version := httputils.VersionFromContext(ctx)
	if versions.LessThan(version, "1.41") {
		// NOTE: filter is a shell glob string applied to repository names.
		filterParam := r.Form.Get("filter")
		if filterParam != "" {
			imageFilters.Add("reference", filterParam)
		}
	}

	var sharedSize bool
	if versions.GreaterThanOrEqualTo(version, "1.42") {
		// NOTE: Support for the "shared-size" parameter was added in API 1.42.
		sharedSize = httputils.BoolValue(r, "shared-size")
	}

	var manifests bool
	if versions.GreaterThanOrEqualTo(version, "1.47") {
		manifests = httputils.BoolValue(r, "manifests")
	}

	images, err := ir.backend.Images(ctx, imagetypes.ListOptions{
		All:        httputils.BoolValue(r, "all"),
		Filters:    imageFilters,
		SharedSize: sharedSize,
		Manifests:  manifests,
	})
	if err != nil {
		return err
	}

	useNone := versions.LessThan(version, "1.43")
	withVirtualSize := versions.LessThan(version, "1.44")
	noDescriptor := versions.LessThan(version, "1.48")
	noContainers := versions.LessThan(version, "1.51")
	for _, img := range images {
		if useNone {
			if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 {
				img.RepoTags = append(img.RepoTags, "<none>:<none>")
				img.RepoDigests = append(img.RepoDigests, "<none>@<none>")
			}
		} else {
			if img.RepoTags == nil {
				img.RepoTags = []string{}
			}
			if img.RepoDigests == nil {
				img.RepoDigests = []string{}
			}
		}
		if withVirtualSize {
			img.VirtualSize = img.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
		}
		if noDescriptor {
			img.Descriptor = nil
		}
		if noContainers {
			img.Containers = -1
		}
	}

	return httputils.WriteJSON(w, http.StatusOK, images)
}

func (ir *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	var platform *ocispec.Platform
	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
		if formPlatform := r.Form.Get("platform"); formPlatform != "" {
			p, err := httputils.DecodePlatform(formPlatform)
			if err != nil {
				return err
			}
			platform = p
		}
	}
	history, err := ir.backend.ImageHistory(ctx, vars["name"], platform)
	if err != nil {
		return err
	}

	return httputils.WriteJSON(w, http.StatusOK, history)
}

func (ir *imageRouter) postImagesTag(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	ref, err := httputils.RepoTagReference(r.Form.Get("repo"), r.Form.Get("tag"))
	if ref == nil || err != nil {
		return errdefs.InvalidParameter(err)
	}

	refName := reference.FamiliarName(ref)
	if refName == string(digest.Canonical) {
		return errdefs.InvalidParameter(errors.New("refusing to create an ambiguous tag using digest algorithm as name"))
	}

	img, err := ir.backend.GetImage(ctx, vars["name"], backend.GetImageOpts{})
	if err != nil {
		return errdefs.NotFound(err)
	}

	if err := ir.backend.TagImage(ctx, img.ID(), ref); err != nil {
		return err
	}
	w.WriteHeader(http.StatusCreated)
	return nil
}

func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	var limit int
	if r.Form.Get("limit") != "" {
		var err error
		limit, err = strconv.Atoi(r.Form.Get("limit"))
		if err != nil || limit < 0 {
			return errdefs.InvalidParameter(errors.Wrap(err, "invalid limit specified"))
		}
	}
	searchFilters, err := filters.FromJSON(r.Form.Get("filters"))
	if err != nil {
		return err
	}

	// For a search it is not an error if no auth was given. Ignore invalid
	// AuthConfig to increase compatibility with the existing API.
	authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))

	headers := http.Header{}
	for k, v := range r.Header {
		k = http.CanonicalHeaderKey(k)
		if strings.HasPrefix(k, "X-Meta-") {
			headers[k] = v
		}
	}
	headers.Set("User-Agent", dockerversion.DockerUserAgent(ctx))
	res, err := ir.searcher.Search(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers)
	if err != nil {
		return err
	}
	return httputils.WriteJSON(w, http.StatusOK, res)
}

func (ir *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
	if err := httputils.ParseForm(r); err != nil {
		return err
	}

	pruneFilters, err := filters.FromJSON(r.Form.Get("filters"))
	if err != nil {
		return err
	}

	pruneReport, err := ir.backend.ImagesPrune(ctx, pruneFilters)
	if err != nil {
		return err
	}
	return httputils.WriteJSON(w, http.StatusOK, pruneReport)
}

// validateRepoName validates the name of a repository.
func validateRepoName(name reference.Named) error {
	familiarName := reference.FamiliarName(name)
	if familiarName == api.NoBaseImageSpecifier {
		return fmt.Errorf("'%s' is a reserved name", familiarName)
	}
	return nil
}
