package libimage

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"

	"github.com/containers/common/pkg/config"
	"github.com/containers/image/v5/docker/reference"
	"github.com/containers/image/v5/pkg/shortnames"
	storageTransport "github.com/containers/image/v5/storage"
	"github.com/containers/image/v5/transports/alltransports"
	"github.com/containers/image/v5/types"
	"github.com/containers/storage"
	deepcopy "github.com/jinzhu/copier"
	jsoniter "github.com/json-iterator/go"
	"github.com/opencontainers/go-digest"
	"github.com/sirupsen/logrus"
)

// Faster than the standard library, see https://github.com/json-iterator/go.
var json = jsoniter.ConfigCompatibleWithStandardLibrary

// tmpdir returns a path to a temporary directory.
func tmpdir() (string, error) {
	var tmpdir string
	defaultContainerConfig, err := config.Default()
	if err == nil {
		tmpdir, err = defaultContainerConfig.ImageCopyTmpDir()
		if err == nil {
			return tmpdir, nil
		}
	}
	return tmpdir, err
}

// RuntimeOptions allow for creating a customized Runtime.
type RuntimeOptions struct {
	// The base system context of the runtime which will be used throughout
	// the entire lifespan of the Runtime.  Certain options in some
	// functions may override specific fields.
	SystemContext *types.SystemContext
}

// setRegistriesConfPath sets the registries.conf path for the specified context.
func setRegistriesConfPath(systemContext *types.SystemContext) {
	if systemContext.SystemRegistriesConfPath != "" {
		return
	}
	if envOverride, ok := os.LookupEnv("CONTAINERS_REGISTRIES_CONF"); ok {
		systemContext.SystemRegistriesConfPath = envOverride
		return
	}
	if envOverride, ok := os.LookupEnv("REGISTRIES_CONFIG_PATH"); ok {
		systemContext.SystemRegistriesConfPath = envOverride
		return
	}
}

// Runtime is responsible for image management and storing them in a containers
// storage.
type Runtime struct {
	// Use to send events out to users.
	eventChannel chan *Event
	// Underlying storage store.
	store storage.Store
	// Global system context.  No pointer to simplify copying and modifying
	// it.
	systemContext types.SystemContext
}

// Returns a copy of the runtime's system context.
func (r *Runtime) SystemContext() *types.SystemContext {
	return r.systemContextCopy()
}

// Returns a copy of the runtime's system context.
func (r *Runtime) systemContextCopy() *types.SystemContext {
	var sys types.SystemContext
	_ = deepcopy.Copy(&sys, &r.systemContext)
	return &sys
}

// EventChannel creates a buffered channel for events that the Runtime will use
// to write events to.  Callers are expected to read from the channel in a
// timely manner.
// Can be called once for a given Runtime.
func (r *Runtime) EventChannel() chan *Event {
	if r.eventChannel != nil {
		return r.eventChannel
	}
	r.eventChannel = make(chan *Event, 100)
	return r.eventChannel
}

// RuntimeFromStore returns a Runtime for the specified store.
func RuntimeFromStore(store storage.Store, options *RuntimeOptions) (*Runtime, error) {
	if options == nil {
		options = &RuntimeOptions{}
	}

	var systemContext types.SystemContext
	if options.SystemContext != nil {
		systemContext = *options.SystemContext
	} else {
		systemContext = types.SystemContext{}
	}
	if systemContext.BigFilesTemporaryDir == "" {
		tmpdir, err := tmpdir()
		if err != nil {
			return nil, err
		}
		systemContext.BigFilesTemporaryDir = tmpdir
	}

	setRegistriesConfPath(&systemContext)

	return &Runtime{
		store:         store,
		systemContext: systemContext,
	}, nil
}

// RuntimeFromStoreOptions returns a return for the specified store options.
func RuntimeFromStoreOptions(runtimeOptions *RuntimeOptions, storeOptions *storage.StoreOptions) (*Runtime, error) {
	if storeOptions == nil {
		storeOptions = &storage.StoreOptions{}
	}
	store, err := storage.GetStore(*storeOptions)
	if err != nil {
		return nil, err
	}
	storageTransport.Transport.SetStore(store)
	return RuntimeFromStore(store, runtimeOptions)
}

// Shutdown attempts to free any kernel resources which are being used by the
// underlying driver.  If "force" is true, any mounted (i.e., in use) layers
// are unmounted beforehand.  If "force" is not true, then layers being in use
// is considered to be an error condition.
func (r *Runtime) Shutdown(force bool) error {
	_, err := r.store.Shutdown(force)
	if r.eventChannel != nil {
		close(r.eventChannel)
	}
	return err
}

// storageToImage transforms a storage.Image to an Image.
func (r *Runtime) storageToImage(storageImage *storage.Image, ref types.ImageReference) *Image {
	return &Image{
		runtime:          r,
		storageImage:     storageImage,
		storageReference: ref,
	}
}

// Exists returns true if the specicifed image exists in the local containers
// storage.  Note that it may return false if an image corrupted.
func (r *Runtime) Exists(name string) (bool, error) {
	image, _, err := r.LookupImage(name, nil)
	if err != nil && !errors.Is(err, storage.ErrImageUnknown) {
		return false, err
	}
	if image == nil {
		return false, nil
	}
	if err := image.isCorrupted(name); err != nil {
		logrus.Error(err)
		return false, nil
	}
	return true, nil
}

// LookupImageOptions allow for customizing local image lookups.
type LookupImageOptions struct {
	// Lookup an image matching the specified architecture.
	Architecture string
	// Lookup an image matching the specified OS.
	OS string
	// Lookup an image matching the specified variant.
	Variant string

	// Controls the behavior when checking the platform of an image.
	PlatformPolicy PlatformPolicy

	// If set, do not look for items/instances in the manifest list that
	// match the current platform but return the manifest list as is.
	// only check for manifest list, return ErrNotAManifestList if not found.
	lookupManifest bool

	// If matching images resolves to a manifest list, return manifest list
	// instead of resolving to image instance, if manifest list is not found
	// try resolving image.
	ManifestList bool

	// If the image resolves to a manifest list, we usually lookup a
	// matching instance and error if none could be found.  In this case,
	// just return the manifest list.  Required for image removal.
	returnManifestIfNoInstance bool
}

var errNoHexValue = errors.New("invalid format: no 64-byte hexadecimal value")

// Lookup Image looks up `name` in the local container storage.  Returns the
// image and the name it has been found with.  Note that name may also use the
// `containers-storage:` prefix used to refer to the containers-storage
// transport.  Returns storage.ErrImageUnknown if the image could not be found.
//
// Unless specified via the options, the image will be looked up by name only
// without matching the architecture, os or variant.  An exception is if the
// image resolves to a manifest list, where an instance of the manifest list
// matching the local or specified platform (via options.{Architecture,OS,Variant})
// is returned.
//
// If the specified name uses the `containers-storage` transport, the resolved
// name is empty.
func (r *Runtime) LookupImage(name string, options *LookupImageOptions) (*Image, string, error) {
	logrus.Debugf("Looking up image %q in local containers storage", name)

	if options == nil {
		options = &LookupImageOptions{}
	}

	// If needed extract the name sans transport.
	storageRef, err := alltransports.ParseImageName(name)
	if err == nil {
		if storageRef.Transport().Name() != storageTransport.Transport.Name() {
			return nil, "", fmt.Errorf("unsupported transport %q for looking up local images", storageRef.Transport().Name())
		}
		img, err := storageTransport.Transport.GetStoreImage(r.store, storageRef)
		if err != nil {
			return nil, "", err
		}
		logrus.Debugf("Found image %q in local containers storage (%s)", name, storageRef.StringWithinTransport())
		return r.storageToImage(img, storageRef), "", nil
	}
	// Docker compat: strip off the tag iff name is tagged and digested
	// (e.g., fedora:latest@sha256...).  In that case, the tag is stripped
	// off and entirely ignored.  The digest is the sole source of truth.
	normalizedName, possiblyUnqualifiedNamedReference, err := normalizeTaggedDigestedString(name)
	if err != nil {
		return nil, "", err
	}
	name = normalizedName

	byDigest := false
	originalName := name
	if strings.HasPrefix(name, "sha256:") {
		byDigest = true
		name = strings.TrimPrefix(name, "sha256:")
	}
	byFullID := reference.IsFullIdentifier(name)

	if byDigest && !byFullID {
		return nil, "", fmt.Errorf("%s: %v", originalName, errNoHexValue)
	}

	// If the name clearly refers to a local image, try to look it up.
	if byFullID || byDigest {
		img, err := r.lookupImageInLocalStorage(originalName, name, nil, options)
		if err != nil {
			return nil, "", err
		}
		if img != nil {
			return img, originalName, nil
		}
		return nil, "", fmt.Errorf("%s: %w", originalName, storage.ErrImageUnknown)
	}

	// Unless specified, set the platform specified in the system context
	// for later platform matching.  Builder likes to set these things via
	// the system context at runtime creation.
	if options.Architecture == "" {
		options.Architecture = r.systemContext.ArchitectureChoice
	}
	if options.OS == "" {
		options.OS = r.systemContext.OSChoice
	}
	if options.Variant == "" {
		options.Variant = r.systemContext.VariantChoice
	}
	// Normalize platform to be OCI compatible (e.g., "aarch64" -> "arm64").
	options.OS, options.Architecture, options.Variant = NormalizePlatform(options.OS, options.Architecture, options.Variant)

	// Second, try out the candidates as resolved by shortnames. This takes
	// "localhost/" prefixed images into account as well.
	candidates, err := shortnames.ResolveLocally(&r.systemContext, name)
	if err != nil {
		return nil, "", fmt.Errorf("%s: %w", name, storage.ErrImageUnknown)
	}
	// Backwards compat: normalize to docker.io as some users may very well
	// rely on that.
	if dockerNamed, err := reference.ParseDockerRef(name); err == nil {
		candidates = append(candidates, dockerNamed)
	}

	for _, candidate := range candidates {
		img, err := r.lookupImageInLocalStorage(name, candidate.String(), candidate, options)
		if err != nil {
			return nil, "", err
		}
		if img != nil {
			return img, candidate.String(), err
		}
	}

	// The specified name may refer to a short ID. Note that this *must*
	// happen after the short-name expansion as done above.
	img, err := r.lookupImageInLocalStorage(name, name, nil, options)
	if err != nil {
		return nil, "", err
	}
	if img != nil {
		return img, name, err
	}

	return r.lookupImageInDigestsAndRepoTags(name, possiblyUnqualifiedNamedReference, options)
}

// lookupImageInLocalStorage looks up the specified candidate for name in the
// storage and checks whether it's matching the system context.
func (r *Runtime) lookupImageInLocalStorage(name, candidate string, namedCandidate reference.Named, options *LookupImageOptions) (*Image, error) {
	logrus.Debugf("Trying %q ...", candidate)

	var err error
	var img *storage.Image
	var ref types.ImageReference

	// FIXME: the lookup logic for manifest lists needs improvement.
	// See https://github.com/containers/common/pull/1505#discussion_r1242677279
	// for details.

	// For images pulled by tag, Image.Names does not currently contain a
	// repo@digest value, so such an input would not match directly in
	// c/storage.
	if namedCandidate != nil {
		namedCandidate = reference.TagNameOnly(namedCandidate)
		ref, err = storageTransport.Transport.NewStoreReference(r.store, namedCandidate, "")
		if err != nil {
			return nil, err
		}
		img, err = storageTransport.Transport.GetStoreImage(r.store, ref)
		if err != nil {
			if errors.Is(err, storage.ErrImageUnknown) {
				return nil, nil
			}
			return nil, err
		}
		// NOTE: we must reparse the reference another time below since
		// an ordinary image may have resolved into a per-platform image
		// without any regard to options.{Architecture,OS,Variant}.
	} else {
		img, err = r.store.Image(candidate)
		if err != nil {
			if errors.Is(err, storage.ErrImageUnknown) {
				return nil, nil
			}
			return nil, err
		}
	}
	ref, err = storageTransport.Transport.ParseStoreReference(r.store, img.ID)
	if err != nil {
		return nil, err
	}

	image := r.storageToImage(img, ref)
	logrus.Debugf("Found image %q as %q in local containers storage", name, candidate)

	// If we referenced a manifest list, we need to check whether we can
	// find a matching instance in the local containers storage.
	isManifestList, err := image.IsManifestList(context.Background())
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			// We must be tolerant toward corrupted images.
			// See containers/podman commit fd9dd7065d44.
			logrus.Warnf("Failed to determine if an image is a manifest list: %v, ignoring the error", err)
			return image, nil
		}
		return nil, err
	}
	if options.lookupManifest || options.ManifestList {
		if isManifestList {
			return image, nil
		}
		// return ErrNotAManifestList if lookupManifest is set otherwise try resolving image.
		if options.lookupManifest {
			return nil, fmt.Errorf("%s: %w", candidate, ErrNotAManifestList)
		}
	}

	if isManifestList {
		logrus.Debugf("Candidate %q is a manifest list, looking up matching instance", candidate)
		manifestList, err := image.ToManifestList()
		if err != nil {
			return nil, err
		}
		instance, err := manifestList.LookupInstance(context.Background(), options.Architecture, options.OS, options.Variant)
		if err != nil {
			if options.returnManifestIfNoInstance {
				logrus.Debug("No matching instance was found: returning manifest list instead")
				return image, nil
			}
			return nil, fmt.Errorf("%v: %w", err, storage.ErrImageUnknown)
		}
		ref, err = storageTransport.Transport.ParseStoreReference(r.store, "@"+instance.ID())
		if err != nil {
			return nil, err
		}
		image = instance
	}

	// Also print the string within the storage transport.  That may aid in
	// debugging when using additional stores since we see explicitly where
	// the store is and which driver (options) are used.
	logrus.Debugf("Found image %q as %q in local containers storage (%s)", name, candidate, ref.StringWithinTransport())

	// Do not perform any further platform checks if the image was
	// requested by ID.  In that case, we must assume that the user/tool
	// know what they're doing.
	if strings.HasPrefix(image.ID(), candidate) {
		return image, nil
	}

	// Ignore the (fatal) error since the image may be corrupted, which
	// will bubble up at other places.  During lookup, we just return it as
	// is.
	if matchError, customPlatform, _ := image.matchesPlatform(context.Background(), options.OS, options.Architecture, options.Variant); matchError != nil {
		if customPlatform {
			logrus.Debugf("%v", matchError)
			// Return nil if the user clearly requested a custom
			// platform and the located image does not match.
			return nil, nil
		}
		switch options.PlatformPolicy {
		case PlatformPolicyDefault:
			logrus.Debugf("%v", matchError)
		case PlatformPolicyWarn:
			logrus.Warnf("%v", matchError)
		}
	}

	return image, nil
}

// lookupImageInDigestsAndRepoTags attempts to match name against any image in
// the local containers storage.  If name is digested, it will be compared
// against image digests.  Otherwise, it will be looked up in the repo tags.
func (r *Runtime) lookupImageInDigestsAndRepoTags(name string, possiblyUnqualifiedNamedReference reference.Named, options *LookupImageOptions) (*Image, string, error) {
	originalName := name // we may change name below

	if possiblyUnqualifiedNamedReference == nil {
		return nil, "", fmt.Errorf("%s: %w", originalName, storage.ErrImageUnknown)
	}
	if !shortnames.IsShortName(name) {
		return nil, "", fmt.Errorf("%s: %w", originalName, storage.ErrImageUnknown)
	}

	var requiredDigest digest.Digest // or ""
	var requiredTag string           // or ""

	possiblyUnqualifiedNamedReference = reference.TagNameOnly(possiblyUnqualifiedNamedReference) // Docker compat: make sure to add the "latest" tag if needed.
	if digested, ok := possiblyUnqualifiedNamedReference.(reference.Digested); ok {
		requiredDigest = digested.Digest()
		name = reference.TrimNamed(possiblyUnqualifiedNamedReference).String()
	} else if namedTagged, ok := possiblyUnqualifiedNamedReference.(reference.NamedTagged); ok {
		requiredTag = namedTagged.Tag()
	} else { // This should never happen after the reference.TagNameOnly above.
		return nil, "", fmt.Errorf("%s: %w (could not cast to tagged)", originalName, storage.ErrImageUnknown)
	}

	allImages, err := r.ListImages(context.Background(), nil, nil)
	if err != nil {
		return nil, "", err
	}

	for _, image := range allImages {
		named, err := image.referenceFuzzilyMatchingRepoAndTag(possiblyUnqualifiedNamedReference, requiredTag)
		if err != nil {
			return nil, "", err
		}
		if named == nil {
			continue
		}
		img, err := r.lookupImageInLocalStorage(name, named.String(), named, options)
		if err != nil {
			return nil, "", err
		}
		if img != nil {
			if requiredDigest != "" {
				if !img.hasDigest(requiredDigest) {
					continue
				}
				named = reference.TrimNamed(named)
				canonical, err := reference.WithDigest(named, requiredDigest)
				if err != nil {
					return nil, "", fmt.Errorf("building canonical reference with digest %q and matched %q: %w", requiredDigest.String(), named.String(), err)
				}
				return img, canonical.String(), nil
			}
			return img, named.String(), nil
		}
	}

	return nil, "", fmt.Errorf("%s: %w", originalName, storage.ErrImageUnknown)
}

// ResolveName resolves the specified name.  If the name resolves to a local
// image, the fully resolved name will be returned.  Otherwise, the name will
// be properly normalized.
//
// Note that an empty string is returned as is.
func (r *Runtime) ResolveName(name string) (string, error) {
	if name == "" {
		return "", nil
	}
	image, resolvedName, err := r.LookupImage(name, nil)
	if err != nil && !errors.Is(err, storage.ErrImageUnknown) {
		return "", err
	}

	if image != nil && !strings.HasPrefix(image.ID(), resolvedName) {
		return resolvedName, err
	}

	normalized, err := NormalizeName(name)
	if err != nil {
		return "", err
	}

	return normalized.String(), nil
}

// IsExternalContainerFunc allows for checking whether the specified container
// is an external one.  The definition of an external container can be set by
// callers.
type IsExternalContainerFunc func(containerID string) (bool, error)

// ListImagesOptions allow for customizing listing images.
type ListImagesOptions struct {
	// Filters to filter the listed images.  Supported filters are
	// * after,before,since=image
	// * containers=true,false,external
	// * dangling=true,false
	// * intermediate=true,false (useful for pruning images)
	// * id=id
	// * label=key[=value]
	// * readonly=true,false
	// * reference=name[:tag] (wildcards allowed)
	Filters []string
	// IsExternalContainerFunc allows for checking whether the specified
	// container is an external one (when containers=external filter is
	// used).  The definition of an external container can be set by
	// callers.
	IsExternalContainerFunc IsExternalContainerFunc
	// SetListData will populate the Image.ListData fields of returned images.
	SetListData bool
}

// ListImages lists images in the local container storage.  If names are
// specified, only images with the specified names are looked up and filtered.
func (r *Runtime) ListImages(ctx context.Context, names []string, options *ListImagesOptions) ([]*Image, error) {
	if options == nil {
		options = &ListImagesOptions{}
	}

	var images []*Image
	if len(names) > 0 {
		for _, name := range names {
			image, _, err := r.LookupImage(name, nil)
			if err != nil {
				return nil, err
			}
			images = append(images, image)
		}
	} else {
		storageImages, err := r.store.Images()
		if err != nil {
			return nil, err
		}
		for i := range storageImages {
			images = append(images, r.storageToImage(&storageImages[i], nil))
		}
	}

	filtered, err := r.filterImages(ctx, images, options)
	if err != nil {
		return nil, err
	}

	if !options.SetListData {
		return filtered, nil
	}

	// If explicitly requested by the user, pre-compute and cache the
	// dangling and parent information of all filtered images.  That will
	// considerably speed things up for callers who need this information
	// as the layer tree will computed once for all instead of once for
	// each individual image (see containers/podman/issues/17828).

	tree, err := r.layerTree(images)
	if err != nil {
		return nil, err
	}

	for i := range filtered {
		isDangling, err := filtered[i].isDangling(ctx, tree)
		if err != nil {
			return nil, err
		}
		filtered[i].ListData.IsDangling = &isDangling

		parent, err := filtered[i].parent(ctx, tree)
		if err != nil {
			return nil, err
		}
		filtered[i].ListData.Parent = parent
	}

	return filtered, nil
}

// RemoveImagesOptions allow for customizing image removal.
type RemoveImagesOptions struct {
	// Force will remove all containers from the local storage that are
	// using a removed image.  Use RemoveContainerFunc for a custom logic.
	// If set, all child images will be removed as well.
	Force bool
	// LookupManifest will expect all specified names to be manifest lists (no instance look up).
	// This allows for removing manifest lists.
	// By default, RemoveImages will attempt to resolve to a manifest instance matching
	// the local platform (i.e., os, architecture, variant).
	LookupManifest bool
	// RemoveContainerFunc allows for a custom logic for removing
	// containers using a specific image.  By default, all containers in
	// the local containers storage will be removed (if Force is set).
	RemoveContainerFunc RemoveContainerFunc
	// Ignore if a specified image does not exist and do not throw an error.
	Ignore bool
	// IsExternalContainerFunc allows for checking whether the specified
	// container is an external one (when containers=external filter is
	// used).  The definition of an external container can be set by
	// callers.
	IsExternalContainerFunc IsExternalContainerFunc
	// Remove external containers even when Force is false.  Requires
	// IsExternalContainerFunc to be specified.
	ExternalContainers bool
	// Filters to filter the removed images.  Supported filters are
	// * after,before,since=image
	// * containers=true,false,external
	// * dangling=true,false
	// * intermediate=true,false (useful for pruning images)
	// * id=id
	// * label=key[=value]
	// * readonly=true,false
	// * reference=name[:tag] (wildcards allowed)
	Filters []string
	// The RemoveImagesReport will include the size of the removed image.
	// This information may be useful when pruning images to figure out how
	// much space was freed. However, computing the size of an image is
	// comparatively expensive, so it is made optional.
	WithSize bool
	// NoPrune will not remove dangling images
	NoPrune bool
}

// RemoveImages removes images specified by names.  If no names are specified,
// remove images as specified via the options' filters.  All images are
// expected to exist in the local containers storage.
//
// If an image has more names than one name, the image will be untagged with
// the specified name.  RemoveImages returns a slice of untagged and removed
// images.
//
// Note that most errors are non-fatal and collected into `rmErrors` return
// value.
func (r *Runtime) RemoveImages(ctx context.Context, names []string, options *RemoveImagesOptions) (reports []*RemoveImageReport, rmErrors []error) {
	if options == nil {
		options = &RemoveImagesOptions{}
	}

	if options.ExternalContainers && options.IsExternalContainerFunc == nil {
		return nil, []error{fmt.Errorf("libimage error: cannot remove external containers without callback")}
	}

	// The logic here may require some explanation.  Image removal is
	// surprisingly complex since it is recursive (intermediate parents are
	// removed) and since multiple items in `names` may resolve to the
	// *same* image.  On top, the data in the containers storage is shared,
	// so we need to be careful and the code must be robust.  That is why
	// users can only remove images via this function; the logic may be
	// complex but the execution path is clear.

	// Bundle an image with a possible empty slice of names to untag.  That
	// allows for a decent untagging logic and to bundle multiple
	// references to the same *Image (and circumvent consistency issues).
	type deleteMe struct {
		image        *Image
		referencedBy []string
	}

	appendError := func(err error) {
		rmErrors = append(rmErrors, err)
	}

	deleteMap := make(map[string]*deleteMe) // ID -> deleteMe
	toDelete := []string{}
	// Look up images in the local containers storage and fill out
	// toDelete and the deleteMap.
	switch {
	case len(names) > 0:
		// prepare lookupOptions
		var lookupOptions *LookupImageOptions
		if options.LookupManifest {
			// LookupManifest configured as true make sure we only remove manifests and no referenced images.
			lookupOptions = &LookupImageOptions{lookupManifest: true}
		} else {
			lookupOptions = &LookupImageOptions{returnManifestIfNoInstance: true}
		}
		// Look up the images one-by-one.  That allows for removing
		// images that have been looked up successfully while reporting
		// lookup errors at the end.
		for _, name := range names {
			img, resolvedName, err := r.LookupImage(name, lookupOptions)
			if err != nil {
				if options.Ignore && errors.Is(err, storage.ErrImageUnknown) {
					continue
				}
				appendError(err)
				continue
			}
			dm, exists := deleteMap[img.ID()]
			if !exists {
				toDelete = append(toDelete, img.ID())
				dm = &deleteMe{image: img}
				deleteMap[img.ID()] = dm
			}
			dm.referencedBy = append(dm.referencedBy, resolvedName)
		}

	default:
		options := &ListImagesOptions{
			IsExternalContainerFunc: options.IsExternalContainerFunc,
			Filters:                 options.Filters,
		}
		filteredImages, err := r.ListImages(ctx, nil, options)
		if err != nil {
			appendError(err)
			return nil, rmErrors
		}
		for _, img := range filteredImages {
			toDelete = append(toDelete, img.ID())
			deleteMap[img.ID()] = &deleteMe{image: img}
		}
	}

	// Return early if there's no image to delete.
	if len(deleteMap) == 0 {
		return nil, rmErrors
	}

	// Now remove the images in the given order.
	rmMap := make(map[string]*RemoveImageReport)
	orderedIDs := []string{}
	visitedIDs := make(map[string]bool)
	for _, id := range toDelete {
		del, exists := deleteMap[id]
		if !exists {
			appendError(fmt.Errorf("internal error: ID %s not in found in image-deletion map", id))
			continue
		}
		if len(del.referencedBy) == 0 {
			del.referencedBy = []string{""}
		}
		for _, ref := range del.referencedBy {
			processedIDs, err := del.image.remove(ctx, rmMap, ref, options)
			if err != nil {
				appendError(err)
			}
			// NOTE: make sure to add given ID only once to orderedIDs.
			for _, id := range processedIDs {
				if visited := visitedIDs[id]; visited {
					continue
				}
				orderedIDs = append(orderedIDs, id)
				visitedIDs[id] = true
			}
		}
	}

	// Finally, we can assemble the reports slice.
	for _, id := range orderedIDs {
		report, exists := rmMap[id]
		if exists {
			reports = append(reports, report)
		}
	}

	return reports, rmErrors
}
