package manifests

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"maps"
	"mime"
	"net/http"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/containers/common/internal"
	"github.com/containers/common/pkg/manifests"
	"github.com/containers/common/pkg/retry"
	"github.com/containers/common/pkg/supplemented"
	cp "github.com/containers/image/v5/copy"
	"github.com/containers/image/v5/docker/reference"
	"github.com/containers/image/v5/image"
	"github.com/containers/image/v5/manifest"
	ocilayout "github.com/containers/image/v5/oci/layout"
	"github.com/containers/image/v5/pkg/compression"
	"github.com/containers/image/v5/signature"
	"github.com/containers/image/v5/signature/signer"
	is "github.com/containers/image/v5/storage"
	"github.com/containers/image/v5/transports"
	"github.com/containers/image/v5/transports/alltransports"
	"github.com/containers/image/v5/types"
	"github.com/containers/storage"
	"github.com/containers/storage/pkg/fileutils"
	"github.com/containers/storage/pkg/ioutils"
	"github.com/containers/storage/pkg/lockfile"
	digest "github.com/opencontainers/go-digest"
	imgspec "github.com/opencontainers/image-spec/specs-go"
	v1 "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/sirupsen/logrus"
)

const (
	defaultMaxRetries = 3
)

const (
	instancesData                = "instances.json"
	artifactsData                = "artifacts.json"
	pushingArtifactsSubdirectory = "referenced-artifacts"
)

// LookupReferenceFunc return an image reference based on the specified one.
// The returned reference can return custom ImageSource or ImageDestination
// objects which intercept or filter blobs, manifests, and signatures as
// they are read and written.
type LookupReferenceFunc func(ref types.ImageReference) (types.ImageReference, error)

// ErrListImageUnknown is returned when we attempt to create an image reference
// for a List that has not yet been saved to an image.
var ErrListImageUnknown = errors.New("unable to determine which image holds the manifest list")

type artifactsDetails struct {
	Manifests map[digest.Digest]string          `json:"manifests,omitempty"` // artifact (and other?) manifest digests ￫ manifest contents
	Files     map[digest.Digest][]string        `json:"files,omitempty"`     // artifact (and other?) manifest digests ￫ file paths (mainly for display)
	Configs   map[digest.Digest]digest.Digest   `json:"config,omitempty"`    // artifact (and other?) manifest digests ￫ referenced config digests
	Layers    map[digest.Digest][]digest.Digest `json:"layers,omitempty"`    // artifact (and other?) manifest digests ￫ referenced layer digests
	Detached  map[digest.Digest]string          `json:"detached,omitempty"`  // "config" and "layer" (and other?) digests in (usually artifact) manifests ￫ file paths
	Blobs     map[digest.Digest][]byte          `json:"blobs,omitempty"`     // "config" and "layer" (and other?) manifest digests ￫ inlined blob contents
}

type list struct {
	manifests.List
	instances map[digest.Digest]string // instance manifest digests ￫ image references
	artifacts artifactsDetails
}

// List is a manifest list or image index, either created using Create(), or
// loaded from local storage using LoadFromImage().
type List interface {
	manifests.List
	SaveToImage(store storage.Store, imageID string, names []string, mimeType string) (string, error)
	Reference(store storage.Store, multiple cp.ImageListSelection, instances []digest.Digest) (types.ImageReference, error)
	Push(ctx context.Context, dest types.ImageReference, options PushOptions) (reference.Canonical, digest.Digest, error)
	Add(ctx context.Context, sys *types.SystemContext, ref types.ImageReference, all bool) (digest.Digest, error)
	AddArtifact(ctx context.Context, sys *types.SystemContext, options AddArtifactOptions, files ...string) (digest.Digest, error)
	InstanceByFile(file string) (digest.Digest, error)
	Files(instanceDigest digest.Digest) ([]string, error)
}

// PushOptions includes various settings which are needed for pushing the
// manifest list and its instances.
type PushOptions struct {
	Store                            storage.Store
	SystemContext                    *types.SystemContext  // github.com/containers/image/types.SystemContext
	ImageListSelection               cp.ImageListSelection // set to either CopySystemImage, CopyAllImages, or CopySpecificImages
	Instances                        []digest.Digest       // instances to copy if ImageListSelection == CopySpecificImages
	ReportWriter                     io.Writer             // will be used to log the writing of the list and any blobs
	Signers                          []*signer.Signer      // if non-empty, asks for signatures to be added during the copy using the provided signers.
	SignBy                           string                // fingerprint of GPG key to use to sign images
	SignPassphrase                   string                // passphrase to use when signing with the key ID from SignBy.
	SignBySigstorePrivateKeyFile     string                // if non-empty, asks for a signature to be added during the copy, using a sigstore private key file at the provided path.
	SignSigstorePrivateKeyPassphrase []byte                // passphrase to use when signing with SignBySigstorePrivateKeyFile.
	RemoveSignatures                 bool                  // true to discard signatures in images
	ManifestType                     string                // the format to use when saving the list - possible options are oci, v2s1, and v2s2
	SourceFilter                     LookupReferenceFunc   // filter the list source
	AddCompression                   []string              // add existing instances with requested compression algorithms to manifest list
	ForceCompressionFormat           bool                  // force push with requested compression ignoring the blobs which can be reused.
	// Maximum number of retries with exponential backoff when facing
	// transient network errors. Default 3.
	MaxRetries *uint
	// RetryDelay used for the exponential back off of MaxRetries.
	RetryDelay *time.Duration
}

// Create creates a new list containing information about the specified image,
// computing its manifest's digest, and retrieving OS and architecture
// information from its configuration blob.  Returns the new list, and the
// instanceDigest for the initial image.
func Create() List {
	return &list{
		List:      manifests.Create(),
		instances: make(map[digest.Digest]string),
		artifacts: artifactsDetails{
			Manifests: make(map[digest.Digest]string),
			Files:     make(map[digest.Digest][]string),
			Configs:   make(map[digest.Digest]digest.Digest),
			Layers:    make(map[digest.Digest][]digest.Digest),
			Detached:  make(map[digest.Digest]string),
			Blobs:     make(map[digest.Digest][]byte),
		},
	}
}

// LoadFromImage reads the manifest list or image index, and additional
// information about where the various instances that it contains live, from an
// image record with the specified ID in local storage.
func LoadFromImage(store storage.Store, image string) (string, List, error) {
	img, err := store.Image(image)
	if err != nil {
		return "", nil, fmt.Errorf("locating image %q for loading manifest list: %w", image, err)
	}
	manifestBytes, err := store.ImageBigData(img.ID, storage.ImageDigestManifestBigDataNamePrefix)
	if err != nil {
		return "", nil, fmt.Errorf("locating image %q for loading manifest list: %w", image, err)
	}
	manifestList, err := manifests.FromBlob(manifestBytes)
	if err != nil {
		return "", nil, err
	}
	list := &list{
		List:      manifestList,
		instances: make(map[digest.Digest]string),
		artifacts: artifactsDetails{
			Manifests: make(map[digest.Digest]string),
			Files:     make(map[digest.Digest][]string),
			Configs:   make(map[digest.Digest]digest.Digest),
			Layers:    make(map[digest.Digest][]digest.Digest),
			Detached:  make(map[digest.Digest]string),
			Blobs:     make(map[digest.Digest][]byte),
		},
	}
	instancesBytes, err := store.ImageBigData(img.ID, instancesData)
	if err != nil {
		return "", nil, fmt.Errorf("locating image %q for loading instance list: %w", image, err)
	}
	if err := json.Unmarshal(instancesBytes, &list.instances); err != nil {
		return "", nil, fmt.Errorf("decoding instance list for image %q: %w", image, err)
	}
	artifactsBytes, err := store.ImageBigData(img.ID, artifactsData)
	if err != nil {
		if !errors.Is(err, os.ErrNotExist) {
			return "", nil, fmt.Errorf("locating image %q for loading instance list: %w", image, err)
		}
		artifactsBytes = []byte("{}")
	}
	if err := json.Unmarshal(artifactsBytes, &list.artifacts); err != nil {
		return "", nil, fmt.Errorf("decoding artifact list for image %q: %w", image, err)
	}
	list.instances[""] = img.ID
	return img.ID, list, nil
}

// SaveToImage saves the manifest list or image index as the manifest of an
// Image record with the specified names in local storage, generating a random
// image ID if none is specified.  It also stores information about where the
// images whose manifests are included in the list can be found.
func (l *list) SaveToImage(store storage.Store, imageID string, names []string, mimeType string) (string, error) {
	manifestBytes, err := l.List.Serialize(mimeType)
	if err != nil {
		return "", err
	}
	instancesBytes, err := json.Marshal(&l.instances)
	if err != nil {
		return "", err
	}
	artifactsBytes, err := json.Marshal(&l.artifacts)
	if err != nil {
		return "", err
	}
	manifestDigest, err := manifest.Digest(manifestBytes)
	if err != nil {
		return "", err
	}
	imageOptions := &storage.ImageOptions{
		BigData: []storage.ImageBigDataOption{
			{Key: storage.ImageDigestManifestBigDataNamePrefix, Data: manifestBytes, Digest: manifestDigest},
			{Key: instancesData, Data: instancesBytes},
			{Key: artifactsData, Data: artifactsBytes},
		},
	}
	img, err := store.CreateImage(imageID, names, "", "", imageOptions)
	if err != nil {
		if imageID != "" && errors.Is(err, storage.ErrDuplicateID) {
			for _, bd := range imageOptions.BigData {
				digester := manifest.Digest
				if !strings.HasPrefix(bd.Key, storage.ImageDigestManifestBigDataNamePrefix) {
					digester = nil
				}
				err := store.SetImageBigData(imageID, bd.Key, bd.Data, digester)
				if err != nil {
					return "", fmt.Errorf("saving manifest list to image %q: %w", imageID, err)
				}
			}
			return imageID, nil
		}
		return "", err
	}
	l.instances[""] = img.ID
	return img.ID, nil
}

// Files returns the list of files associated with a particular artifact
// instance in the image index, primarily for display purposes.
func (l *list) Files(instanceDigest digest.Digest) ([]string, error) {
	return slices.Clone(l.artifacts.Files[instanceDigest]), nil
}

// instanceByFile returns the instanceDigest of the first manifest in the index
// which refers to the named file.  The name will be passed to filepath.Abs()
// before searching for an instance which references it.
func (l *list) InstanceByFile(file string) (digest.Digest, error) {
	if parsedDigest, err := digest.Parse(file); err == nil {
		// nice try, but that's already a digest!
		return parsedDigest, nil
	}
	abs, err := filepath.Abs(file)
	if err != nil {
		return "", err
	}
	for instanceDigest, files := range l.artifacts.Files {
		for _, file := range files {
			if file == abs {
				return instanceDigest, nil
			}
		}
	}
	return "", os.ErrNotExist
}

// Reference returns an image reference for the composite image being built
// in the list, or an error if the list has never been saved to a local image.
func (l *list) Reference(store storage.Store, multiple cp.ImageListSelection, instances []digest.Digest) (types.ImageReference, error) {
	if l.instances[""] == "" {
		return nil, fmt.Errorf("building reference to list, appears to have not been saved first: %w", ErrListImageUnknown)
	}
	s, err := is.Transport.ParseStoreReference(store, l.instances[""])
	if err != nil {
		return nil, fmt.Errorf("creating ImageReference from image %q: %w", l.instances[""], err)
	}
	references := make([]types.ImageReference, 0, len(l.instances))
	whichInstances := make([]digest.Digest, 0, len(l.instances))
	switch multiple {
	case cp.CopyAllImages, cp.CopySystemImage:
		for instance := range l.instances {
			if instance != "" {
				whichInstances = append(whichInstances, instance)
			}
		}
	case cp.CopySpecificImages:
		for instance := range l.instances {
			if slices.Contains(instances, instance) {
				whichInstances = append(whichInstances, instance)
			}
		}
	}
	if len(l.artifacts.Manifests) > 0 {
		img, err := is.Transport.GetImage(s)
		if err != nil {
			return nil, fmt.Errorf("locating image %s: %w", transports.ImageName(s), err)
		}
		imgDirectory, err := store.ImageDirectory(img.ID)
		if err != nil {
			return nil, fmt.Errorf("locating per-image directory for %s: %w", img.ID, err)
		}
		tmp, err := os.MkdirTemp(imgDirectory, pushingArtifactsSubdirectory)
		if err != nil {
			return nil, err
		}
		subdir := 0
		for artifactManifestDigest, contents := range l.artifacts.Manifests {
			// create the blobs directory
			subdir++
			tmp := filepath.Join(tmp, strconv.Itoa(subdir))
			blobsDir := filepath.Join(tmp, "blobs", artifactManifestDigest.Algorithm().String())
			if err := os.MkdirAll(blobsDir, 0o700); err != nil {
				return nil, fmt.Errorf("creating directory for blobs: %w", err)
			}
			// write the artifact manifest
			if err := os.WriteFile(filepath.Join(blobsDir, artifactManifestDigest.Encoded()), []byte(contents), 0o644); err != nil {
				return nil, fmt.Errorf("writing artifact manifest as blob: %w", err)
			}
			// symlink all of the referenced files and write the inlined blobs into the blobs directory
			var referencedBlobDigests []digest.Digest
			var symlinkedFiles []string
			if referencedConfigDigest, ok := l.artifacts.Configs[artifactManifestDigest]; ok {
				referencedBlobDigests = append(referencedBlobDigests, referencedConfigDigest)
			}
			referencedBlobDigests = append(referencedBlobDigests, l.artifacts.Layers[artifactManifestDigest]...)
			for _, referencedBlobDigest := range referencedBlobDigests {
				referencedFile, knownFile := l.artifacts.Detached[referencedBlobDigest]
				referencedBlob, knownBlob := l.artifacts.Blobs[referencedBlobDigest]
				if !knownFile && !knownBlob {
					return nil, fmt.Errorf(`internal error: no file or blob with artifact "config" or "layer" digest %q recorded`, referencedBlobDigest)
				}
				expectedLayerBlobPath := filepath.Join(blobsDir, referencedBlobDigest.Encoded())
				if err := fileutils.Lexists(expectedLayerBlobPath); err == nil {
					// did this one already
					continue
				} else if knownFile {
					if err := os.Symlink(referencedFile, expectedLayerBlobPath); err != nil {
						return nil, err
					}
					symlinkedFiles = append(symlinkedFiles, referencedFile)
				} else if knownBlob {
					if err := os.WriteFile(expectedLayerBlobPath, referencedBlob, 0o600); err != nil {
						return nil, err
					}
				}
			}
			// write the index that refers to this one artifact image
			indexFile := filepath.Join(tmp, v1.ImageIndexFile)
			index := v1.Index{
				Versioned: imgspec.Versioned{
					SchemaVersion: 2,
				},
				MediaType: v1.MediaTypeImageIndex,
				Manifests: []v1.Descriptor{{
					MediaType: v1.MediaTypeImageManifest,
					Digest:    artifactManifestDigest,
					Size:      int64(len(contents)),
				}},
			}
			indexBytes, err := json.Marshal(&index)
			if err != nil {
				return nil, fmt.Errorf("encoding image index for OCI layout: %w", err)
			}
			if err := os.WriteFile(indexFile, indexBytes, 0o644); err != nil {
				return nil, fmt.Errorf("writing image index for OCI layout: %w", err)
			}
			// write the layout file
			layoutFile := filepath.Join(tmp, v1.ImageLayoutFile)
			layoutBytes, err := json.Marshal(v1.ImageLayout{Version: v1.ImageLayoutVersion})
			if err != nil {
				return nil, fmt.Errorf("encoding image layout for OCI layout: %w", err)
			}
			if err := os.WriteFile(layoutFile, layoutBytes, 0o644); err != nil {
				return nil, fmt.Errorf("writing oci-layout file: %w", err)
			}
			// build the reference to this artifact image's oci layout
			ref, err := ocilayout.NewReference(tmp, "")
			if err != nil {
				return nil, fmt.Errorf("creating ImageReference for artifact with files %q: %w", symlinkedFiles, err)
			}
			references = append(references, ref)
		}
	}
	for _, instance := range whichInstances {
		imageName := l.instances[instance]
		ref, err := alltransports.ParseImageName(imageName)
		if err != nil {
			return nil, fmt.Errorf("creating ImageReference from image %q: %w", imageName, err)
		}
		references = append(references, ref)
	}
	return supplemented.Reference(s, references, multiple, instances), nil
}

// Push saves the manifest list and whichever blobs are needed to a destination location.
func (l *list) Push(ctx context.Context, dest types.ImageReference, options PushOptions) (reference.Canonical, digest.Digest, error) {
	// Load the system signing policy.
	pushPolicy, err := signature.DefaultPolicy(options.SystemContext)
	if err != nil {
		return nil, "", fmt.Errorf("obtaining default signature policy: %w", err)
	}

	// Override the settings for local storage to make sure that we can always read the source "image".
	pushPolicy.Transports[is.Transport.Name()] = storageAllowedPolicyScopes

	policyContext, err := signature.NewPolicyContext(pushPolicy)
	if err != nil {
		return nil, "", fmt.Errorf("creating new signature policy context: %w", err)
	}
	defer func() {
		if err2 := policyContext.Destroy(); err2 != nil {
			logrus.Errorf("Destroying signature policy context: %v", err2)
		}
	}()

	// If we were given a media type that corresponds to a multiple-images
	// type, reset it to a valid corresponding single-image type, since we
	// already expect the image library to infer the list type from the
	// image type that we're telling it to force.
	singleImageManifestType := options.ManifestType
	switch singleImageManifestType {
	case v1.MediaTypeImageIndex:
		singleImageManifestType = v1.MediaTypeImageManifest
	case manifest.DockerV2ListMediaType:
		singleImageManifestType = manifest.DockerV2Schema2MediaType
	}

	// Build a source reference for our list and grab bag full of blobs.
	src, err := l.Reference(options.Store, options.ImageListSelection, options.Instances)
	if err != nil {
		return nil, "", err
	}
	if options.SourceFilter != nil {
		if src, err = options.SourceFilter(src); err != nil {
			return nil, "", err
		}
	}
	compressionVariants, err := prepareAddWithCompression(options.AddCompression)
	if err != nil {
		return nil, "", err
	}
	copyOptions := &cp.Options{
		ImageListSelection:               options.ImageListSelection,
		Instances:                        options.Instances,
		SourceCtx:                        options.SystemContext,
		DestinationCtx:                   options.SystemContext,
		ReportWriter:                     options.ReportWriter,
		RemoveSignatures:                 options.RemoveSignatures,
		Signers:                          options.Signers,
		SignBy:                           options.SignBy,
		SignPassphrase:                   options.SignPassphrase,
		SignBySigstorePrivateKeyFile:     options.SignBySigstorePrivateKeyFile,
		SignSigstorePrivateKeyPassphrase: options.SignSigstorePrivateKeyPassphrase,
		ForceManifestMIMEType:            singleImageManifestType,
		EnsureCompressionVariantsExist:   compressionVariants,
		ForceCompressionFormat:           options.ForceCompressionFormat,
	}

	retryOptions := retry.Options{}
	retryOptions.MaxRetry = defaultMaxRetries
	if options.MaxRetries != nil {
		retryOptions.MaxRetry = int(*options.MaxRetries)
	}
	if options.RetryDelay != nil {
		retryOptions.Delay = *options.RetryDelay
	}

	// Copy whatever we were asked to copy.
	var manifestDigest digest.Digest
	f := func() error {
		opts := copyOptions
		var manifestBytes []byte
		var digest digest.Digest
		var err error
		if manifestBytes, err = cp.Image(ctx, policyContext, dest, src, opts); err == nil {
			if digest, err = manifest.Digest(manifestBytes); err == nil {
				manifestDigest = digest
			}
		}
		return err
	}
	err = retry.IfNecessary(ctx, f, &retryOptions)
	return nil, manifestDigest, err
}

func prepareAddWithCompression(variants []string) ([]cp.OptionCompressionVariant, error) {
	res := []cp.OptionCompressionVariant{}
	for _, name := range variants {
		algo, err := compression.AlgorithmByName(name)
		if err != nil {
			return nil, fmt.Errorf("requested algorithm %s is not supported for replication: %w", name, err)
		}
		res = append(res, cp.OptionCompressionVariant{Algorithm: algo})
	}
	return res, nil
}

func mapToSlice(m map[string]string) []string {
	slice := make([]string, 0, len(m))
	for key, value := range m {
		slice = append(slice, key+"="+value)
	}
	return slice
}

// Add adds information about the specified image to the list, computing the
// image's manifest's digest, retrieving OS and architecture information from
// the image's configuration, and recording the image's reference so that it
// can be found at push-time.  Returns the instanceDigest for the image.  If
// the reference points to an image list, either all instances are added (if
// "all" is true), or the instance which matches "sys" (if "all" is false) will
// be added.
func (l *list) Add(ctx context.Context, sys *types.SystemContext, ref types.ImageReference, all bool) (digest.Digest, error) {
	src, err := ref.NewImageSource(ctx, sys)
	if err != nil {
		return "", fmt.Errorf("setting up to read manifest and configuration from %q: %w", transports.ImageName(ref), err)
	}
	defer src.Close()

	type instanceInfo struct {
		instanceDigest                       *digest.Digest
		OS, Architecture, OSVersion, Variant string
		Features, OSFeatures, Annotations    []string
		Size                                 int64
		ConfigInfo                           types.BlobInfo
		ArtifactType                         string
		URLs                                 []string
	}
	var instanceInfos []instanceInfo
	var manifestDigest digest.Digest

	primaryManifestBytes, primaryManifestType, err := src.GetManifest(ctx, nil)
	if err != nil {
		return "", fmt.Errorf("reading manifest from %q: %w", transports.ImageName(ref), err)
	}

	if manifest.MIMETypeIsMultiImage(primaryManifestType) {
		lists, err := manifests.FromBlob(primaryManifestBytes)
		if err != nil {
			return "", fmt.Errorf("parsing manifest list in %q: %w", transports.ImageName(ref), err)
		}
		if all {
			for i, instance := range lists.OCIv1().Manifests {
				platform := instance.Platform
				if platform == nil {
					platform = &v1.Platform{}
				}
				instanceDigest := instance.Digest
				instanceInfo := instanceInfo{
					instanceDigest: &instanceDigest,
					OS:             platform.OS,
					Architecture:   platform.Architecture,
					OSVersion:      platform.OSVersion,
					Variant:        platform.Variant,
					Features:       append([]string{}, lists.Docker().Manifests[i].Platform.Features...),
					OSFeatures:     append([]string{}, platform.OSFeatures...),
					Size:           instance.Size,
					ArtifactType:   instance.ArtifactType,
					Annotations:    mapToSlice(instance.Annotations),
					URLs:           instance.URLs,
				}
				instanceInfos = append(instanceInfos, instanceInfo)
			}
		} else {
			list, err := manifest.ListFromBlob(primaryManifestBytes, primaryManifestType)
			if err != nil {
				return "", fmt.Errorf("parsing manifest list in %q: %w", transports.ImageName(ref), err)
			}
			instanceDigest, err := list.ChooseInstance(sys)
			if err != nil {
				return "", fmt.Errorf("selecting image from manifest list in %q: %w", transports.ImageName(ref), err)
			}
			added := false
			for i, instance := range lists.OCIv1().Manifests {
				if instance.Digest != instanceDigest {
					continue
				}
				platform := instance.Platform
				if platform == nil {
					platform = &v1.Platform{}
				}
				instanceInfo := instanceInfo{
					instanceDigest: &instanceDigest,
					OS:             platform.OS,
					Architecture:   platform.Architecture,
					OSVersion:      platform.OSVersion,
					Variant:        platform.Variant,
					Features:       append([]string{}, lists.Docker().Manifests[i].Platform.Features...),
					OSFeatures:     append([]string{}, platform.OSFeatures...),
					Size:           instance.Size,
					ArtifactType:   instance.ArtifactType,
					Annotations:    mapToSlice(instance.Annotations),
					URLs:           instance.URLs,
				}
				instanceInfos = append(instanceInfos, instanceInfo)
				added = true
			}
			if !added {
				instanceInfo := instanceInfo{
					instanceDigest: &instanceDigest,
				}
				instanceInfos = append(instanceInfos, instanceInfo)
			}
		}
	} else {
		instanceInfo := instanceInfo{
			instanceDigest: nil,
		}
		if primaryManifestType == v1.MediaTypeImageManifest {
			if m, err := manifest.OCI1FromManifest(primaryManifestBytes); err == nil {
				instanceInfo.ArtifactType = m.ArtifactType
			}
		}
		instanceInfos = append(instanceInfos, instanceInfo)
	}

	knownConfigTypes := []string{manifest.DockerV2Schema2ConfigMediaType, v1.MediaTypeImageConfig}
	for _, instanceInfo := range instanceInfos {
		manifestBytes, manifestType, err := src.GetManifest(ctx, instanceInfo.instanceDigest)
		if err != nil {
			return "", fmt.Errorf("reading manifest from %q, instance %q: %w", transports.ImageName(ref), instanceInfo.instanceDigest, err)
		}
		instanceManifest, err := manifest.FromBlob(manifestBytes, manifestType)
		if err != nil {
			return "", fmt.Errorf("parsing manifest from %q, instance %q: %w", transports.ImageName(ref), instanceInfo.instanceDigest, err)
		}
		instanceInfo.ConfigInfo = instanceManifest.ConfigInfo()
		hasPlatformConfig := instanceInfo.ArtifactType == "" && slices.Contains(knownConfigTypes, instanceInfo.ConfigInfo.MediaType)
		needToParsePlatformConfig := (instanceInfo.OS == "" || instanceInfo.Architecture == "")
		if hasPlatformConfig && needToParsePlatformConfig {
			img, err := image.FromUnparsedImage(ctx, sys, image.UnparsedInstance(src, instanceInfo.instanceDigest))
			if err != nil {
				return "", fmt.Errorf("reading configuration blob from %q: %w", transports.ImageName(ref), err)
			}
			config, err := img.OCIConfig(ctx)
			if err != nil {
				return "", fmt.Errorf("reading info about config blob from %q: %w", transports.ImageName(ref), err)
			}
			if instanceInfo.OS == "" {
				instanceInfo.OS = config.OS
				instanceInfo.OSVersion = config.OSVersion
				instanceInfo.OSFeatures = slices.Clone(config.OSFeatures)
			}
			if instanceInfo.Architecture == "" {
				instanceInfo.Architecture = config.Architecture
				instanceInfo.Variant = config.Variant
			}
		}
		if instanceInfo.instanceDigest == nil {
			manifestDigest, err = manifest.Digest(manifestBytes)
			if err != nil {
				return "", fmt.Errorf("computing digest of manifest from %q: %w", transports.ImageName(ref), err)
			}
			instanceInfo.instanceDigest = &manifestDigest
			instanceInfo.Size = int64(len(manifestBytes))
		} else if manifestDigest == "" {
			manifestDigest = *instanceInfo.instanceDigest
		}
		err = l.List.AddInstance(*instanceInfo.instanceDigest, instanceInfo.Size, manifestType, instanceInfo.OS, instanceInfo.Architecture, instanceInfo.OSVersion, instanceInfo.OSFeatures, instanceInfo.Variant, instanceInfo.Features, instanceInfo.Annotations)
		if err != nil {
			return "", fmt.Errorf("adding instance with digest %q: %w", *instanceInfo.instanceDigest, err)
		}
		if err := l.List.SetArtifactType(instanceInfo.instanceDigest, instanceInfo.ArtifactType); err != nil {
			return "", fmt.Errorf("setting artifact manifest type for instance with digest %q: %w", *instanceInfo.instanceDigest, err)
		}
		if err = l.List.SetURLs(*instanceInfo.instanceDigest, instanceInfo.URLs); err != nil {
			return "", fmt.Errorf("setting URLs for instance with digest %q: %w", *instanceInfo.instanceDigest, err)
		}
		if _, ok := l.instances[*instanceInfo.instanceDigest]; !ok {
			l.instances[*instanceInfo.instanceDigest] = transports.ImageName(ref)
		}
	}

	return manifestDigest, nil
}

// AddArtifactOptions contains options which control the contents of the
// artifact manifest that AddArtifact will create and add to the image index.

// This should provide for all of the ways to construct a manifest outlined in
// https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidelines-for-artifact-usage
//   - no blobs ￫ set ManifestArtifactType
//   - blobs, no configuration ￫ set ManifestArtifactType and possibly LayerMediaType, and provide file names
//   - blobs and configuration ￫ set ManifestArtifactType, possibly LayerMediaType, and ConfigDescriptor, and provide file names
//
// The older style of describing artifacts:
//   - leave ManifestArtifactType blank
//   - specify a zero-length application/vnd.oci.image.config.v1+json config blob
//   - set LayerMediaType to a custom type
//
// When reading data produced elsewhere, note that newer tooling will produce
// manifests with ArtifactType set.  If the manifest's ArtifactType is not set,
// consumers should consult the config descriptor's MediaType.
type AddArtifactOptions struct {
	ManifestArtifactType *string              // overall type of the artifact manifest. default: "application/vnd.unknown.artifact.v1"
	Platform             v1.Platform          // default: add to the index without platform information
	ConfigDescriptor     *v1.Descriptor       // default: a descriptor for an explicitly empty config blob
	ConfigFile           string               // path to config contents, recorded if ConfigDescriptor.Size != 0 and ConfigDescriptor.Data is not set
	LayerMediaType       *string              // default: mime.TypeByExtension() if basename contains ".", else http.DetectContentType()
	Annotations          map[string]string    // optional, default is none
	SubjectReference     types.ImageReference // optional
	ExcludeTitles        bool                 // don't add "org.opencontainers.image.title" annotations set to file base names
}

// AddArtifact creates an artifact manifest describing the specified file or
// files, then adds them to the specified image index.  Returns the
// instanceDigest for the artifact manifest.
// The caller could craft the manifest themselves and use Add() to add it to
// the image index and get the same end-result, but this should save them some
// work.
func (l *list) AddArtifact(ctx context.Context, sys *types.SystemContext, options AddArtifactOptions, files ...string) (digest.Digest, error) {
	// If we were given a subject, build a descriptor for it first, since
	// it might be remote, and anything else we do before looking at it
	// might have to get thrown away if we can't get to it for whatever
	// reason.
	var subject *v1.Descriptor
	if options.SubjectReference != nil {
		subjectReference, err := options.SubjectReference.NewImageSource(ctx, sys)
		if err != nil {
			return "", fmt.Errorf("setting up to read manifest and configuration from subject %q: %w", transports.ImageName(options.SubjectReference), err)
		}
		defer subjectReference.Close()
		subjectManifestBytes, subjectManifestType, err := subjectReference.GetManifest(ctx, nil)
		if err != nil {
			return "", fmt.Errorf("reading manifest from subject %q: %w", transports.ImageName(options.SubjectReference), err)
		}
		subjectManifestDigest, err := manifest.Digest(subjectManifestBytes)
		if err != nil {
			return "", fmt.Errorf("digesting manifest of subject %q: %w", transports.ImageName(options.SubjectReference), err)
		}
		var subjectArtifactType string
		if !manifest.MIMETypeIsMultiImage(subjectManifestType) {
			var subjectManifest v1.Manifest
			if json.Unmarshal(subjectManifestBytes, &subjectManifest) == nil {
				subjectArtifactType = subjectManifest.ArtifactType
			}
		}
		subject = &v1.Descriptor{
			MediaType:    subjectManifestType,
			ArtifactType: subjectArtifactType,
			Digest:       subjectManifestDigest,
			Size:         int64(len(subjectManifestBytes)),
		}
	}

	// Build up the layers list piece by piece.
	var layers []v1.Descriptor
	fileDigests := make(map[string]digest.Digest)

	if len(files) == 0 {
		// https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidelines-for-artifact-usage
		// says that we should have at least one layer listed, even if it's just a placeholder
		layers = append(layers, v1.DescriptorEmptyJSON)
	}
	for _, file := range files {
		if err := func() error {
			// Open the file so that we can digest it.
			absFile, err := filepath.Abs(file)
			if err != nil {
				return fmt.Errorf("converting %q to an absolute path: %w", file, err)
			}

			f, err := os.Open(absFile)
			if err != nil {
				return fmt.Errorf("reading %q to determine its digest: %w", file, err)
			}
			defer f.Close()

			// Hang on to a copy of the first 512 bytes, but digest the whole thing.
			digester := digest.Canonical.Digester()
			writeCounter := ioutils.NewWriteCounter(digester.Hash())
			var detectableData bytes.Buffer
			_, err = io.CopyN(writeCounter, io.TeeReader(f, &detectableData), 512)
			if err != nil && !errors.Is(err, io.EOF) {
				return fmt.Errorf("reading %q to determine its digest: %w", file, err)
			}
			if err == nil {
				if _, err := io.Copy(writeCounter, f); err != nil {
					return fmt.Errorf("reading %q to determine its digest: %w", file, err)
				}
			}
			fileDigests[absFile] = digester.Digest()

			// If one wasn't specified, figure out what the MediaType should be.
			title := filepath.Base(absFile)
			layerMediaType := options.LayerMediaType
			if layerMediaType == nil {
				if index := strings.LastIndex(title, "."); index != -1 {
					// File's basename has an extension, try to use a shortcut.
					tmp := mime.TypeByExtension(title[index:])
					if tmp != "" {
						layerMediaType = &tmp
					}
				}
				if layerMediaType == nil {
					// File's basename has no extension or didn't map to a type, look at the contents we saved.
					tmp := http.DetectContentType(detectableData.Bytes())
					layerMediaType = &tmp
				}
				if layerMediaType != nil {
					// Strip off any parameters, since we only want the type name.
					if parsedMediaType, _, err := mime.ParseMediaType(*layerMediaType); err == nil {
						layerMediaType = &parsedMediaType
					}
				}
			}

			// Build the descriptor for the layer.
			descriptor := v1.Descriptor{
				MediaType: *layerMediaType,
				Digest:    fileDigests[absFile],
				Size:      writeCounter.Count,
			}
			// OCI annotations are usually applied at the image manifest as a whole,
			// but tools like oras (https://oras.land/) also apply them to blob
			// descriptors.  AnnotationTitle is used as a suggestion for the name
			// to give to a blob if it's being stored as a file, and we default
			// to adding one based on its original name.
			if !options.ExcludeTitles {
				descriptor.Annotations = map[string]string{
					v1.AnnotationTitle: title,
				}
			}
			layers = append(layers, descriptor)
			return nil
		}(); err != nil {
			return "", err
		}
	}

	// Unless we were told what this is, use the default that ORAS uses.
	artifactType := "application/vnd.unknown.artifact.v1"
	if options.ManifestArtifactType != nil {
		artifactType = *options.ManifestArtifactType
	}

	// Unless we were explicitly told otherwise, default to an empty config blob.
	configDescriptor := internal.DeepCopyDescriptor(&v1.DescriptorEmptyJSON)
	if options.ConfigDescriptor != nil {
		configDescriptor = internal.DeepCopyDescriptor(options.ConfigDescriptor)
	}
	if options.ConfigFile != "" {
		if options.ConfigDescriptor == nil { // i.e., we assigned the default mediatype
			configDescriptor.MediaType = v1.MediaTypeImageConfig
		}
		configDescriptor.Data = nil
		configDescriptor.Digest = "" // to be figured out below
		configDescriptor.Size = -1   // to be figured out below
	}
	configFilePath := ""
	if configDescriptor.Size != 0 {
		if len(configDescriptor.Data) == 0 {
			if options.ConfigFile == "" {
				return "", errors.New("needed config data file, but none was provided")
			}
			filePath, err := filepath.Abs(options.ConfigFile)
			if err != nil {
				return "", fmt.Errorf("recording artifact config data file %q: %w", options.ConfigFile, err)
			}
			digester := digest.Canonical.Digester()
			counter := ioutils.NewWriteCounter(digester.Hash())
			if err := func() error {
				f, err := os.Open(filePath)
				if err != nil {
					return fmt.Errorf("reading artifact config data file %q: %w", options.ConfigFile, err)
				}
				defer f.Close()
				if _, err := io.Copy(counter, f); err != nil {
					return fmt.Errorf("digesting artifact config data file %q: %w", options.ConfigFile, err)
				}
				return nil
			}(); err != nil {
				return "", err
			}
			configDescriptor.Data = nil
			configDescriptor.Size = counter.Count
			configDescriptor.Digest = digester.Digest()
			configFilePath = filePath
		} else {
			decoder := bytes.NewReader(configDescriptor.Data)
			digester := digest.Canonical.Digester()
			counter := ioutils.NewWriteCounter(digester.Hash())
			if _, err := io.Copy(counter, decoder); err != nil {
				return "", fmt.Errorf("digesting inlined artifact config data: %w", err)
			}
			configDescriptor.Size = counter.Count
			configDescriptor.Digest = digester.Digest()
		}
	} else {
		configDescriptor.Data = nil
		configDescriptor.Digest = digest.Canonical.FromString("")
	}

	// Construct the manifest.
	artifactManifest := v1.Manifest{
		Versioned: imgspec.Versioned{
			SchemaVersion: 2,
		},
		MediaType:    v1.MediaTypeImageManifest,
		ArtifactType: artifactType,
		Config:       *configDescriptor,
		Layers:       layers,
		Subject:      subject,
	}
	// Add in annotations, more or less exactly as specified.
	artifactManifest.Annotations = maps.Clone(options.Annotations)

	// Encode and save the data we care about.
	artifactManifestBytes, err := json.Marshal(artifactManifest)
	if err != nil {
		return "", fmt.Errorf("marshalling the artifact manifest: %w", err)
	}
	artifactManifestDigest, err := manifest.Digest(artifactManifestBytes)
	if err != nil {
		return "", fmt.Errorf("digesting the artifact manifest: %w", err)
	}
	l.artifacts.Manifests[artifactManifestDigest] = string(artifactManifestBytes)
	l.artifacts.Layers[artifactManifestDigest] = nil
	l.artifacts.Configs[artifactManifestDigest] = artifactManifest.Config.Digest
	if configFilePath != "" {
		l.artifacts.Detached[artifactManifest.Config.Digest] = configFilePath
		l.artifacts.Files[artifactManifestDigest] = append(l.artifacts.Files[artifactManifestDigest], configFilePath)
	} else {
		l.artifacts.Blobs[artifactManifest.Config.Digest] = slices.Clone(artifactManifest.Config.Data)
	}
	for filePath, fileDigest := range fileDigests {
		l.artifacts.Layers[artifactManifestDigest] = append(l.artifacts.Layers[artifactManifestDigest], fileDigest)
		l.artifacts.Detached[fileDigest] = filePath
		l.artifacts.Files[artifactManifestDigest] = append(l.artifacts.Files[artifactManifestDigest], filePath)
	}
	for _, layer := range layers {
		if len(layer.Data) != 0 {
			l.artifacts.Blobs[layer.Digest] = slices.Clone(layer.Data)
			l.artifacts.Layers[artifactManifestDigest] = append(l.artifacts.Layers[artifactManifestDigest], layer.Digest)
		}
	}
	// Add this artifact manifest to the image index.
	if err := l.AddInstance(artifactManifestDigest, int64(len(artifactManifestBytes)), artifactManifest.MediaType, options.Platform.OS, options.Platform.Architecture, options.Platform.OSVersion, options.Platform.OSFeatures, options.Platform.Variant, nil, nil); err != nil {
		return "", fmt.Errorf("adding artifact manifest for %q to image index: %w", files, err)
	}
	// Set the artifact type in the image index entry if we have one, since AddInstance() didn't do that for us.
	if artifactManifest.ArtifactType != "" {
		if err := l.List.SetArtifactType(&artifactManifestDigest, artifactManifest.ArtifactType); err != nil {
			return "", fmt.Errorf("adding artifact manifest for %q to image index: %w", files, err)
		}
	}
	return artifactManifestDigest, nil
}

// Remove filters out any instances in the list which match the specified digest.
func (l *list) Remove(instanceDigest digest.Digest) error {
	err := l.List.Remove(instanceDigest)
	if err == nil {
		delete(l.instances, instanceDigest)
	}
	return err
}

// LockerForImage returns a Locker for a given image record.  It's recommended
// that processes which use LoadFromImage() to load a list from an image and
// then use that list's SaveToImage() method to save a modified version of the
// list to that image record use this lock to avoid accidentally wiping out
// changes that another process is also attempting to make.
func LockerForImage(store storage.Store, image string) (lockfile.Locker, error) { // nolint:staticcheck
	img, err := store.Image(image)
	if err != nil {
		return nil, fmt.Errorf("locating image %q for locating lock: %w", image, err)
	}
	d := digest.NewDigestFromEncoded(digest.Canonical, img.ID)
	if err := d.Validate(); err != nil {
		return nil, fmt.Errorf("coercing image ID for %q into a digest: %w", image, err)
	}
	return store.GetDigestLock(d)
}
