package imagetools

import (
	"bytes"
	"context"
	"encoding/json"
	"maps"
	"os"
	"strings"

	"github.com/containerd/containerd/v2/core/content"
	"github.com/containerd/containerd/v2/core/images"
	"github.com/containerd/containerd/v2/core/remotes"
	"github.com/containerd/errdefs"
	"github.com/containerd/platforms"
	"github.com/moby/buildkit/client/ociindex"
	"github.com/moby/buildkit/exporter/containerimage/exptypes"
	"github.com/moby/buildkit/util/attestation"
	"github.com/moby/buildkit/util/contentutil"
	"github.com/opencontainers/go-digest"
	"github.com/opencontainers/image-spec/specs-go"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
	"golang.org/x/sync/errgroup"
)

const (
	artifactTypeAttestationManifest = "application/vnd.docker.attestation.manifest.v1+json"
	artifactTypeCosignSignature     = "application/vnd.dev.cosign.artifact.sig.v1+json"
	artifactTypeSigstoreBundle      = "application/vnd.dev.sigstore.bundle.v0.3+json"
)

var supportedArtifactTypes = map[string]struct{}{
	artifactTypeAttestationManifest: {},
	artifactTypeCosignSignature:     {},
	artifactTypeSigstoreBundle:      {},
}

type Source struct {
	Desc ocispecs.Descriptor
	Ref  *Location
}

func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann map[exptypes.AnnotationKey]string, preferIndex bool, platforms []ocispecs.Platform) ([]byte, ocispecs.Descriptor, []DescWithSource, error) {
	dt, desc, srcMap, err := r.combine(ctx, srcs, ann, preferIndex)
	if err != nil {
		return nil, ocispecs.Descriptor{}, nil, err
	}
	dt, desc, mfstsWithSource, err := r.filterPlatforms(ctx, dt, desc, srcMap, platforms)
	if err != nil {
		return nil, ocispecs.Descriptor{}, nil, err
	}
	return dt, desc, mfstsWithSource, nil
}

func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes.AnnotationKey]string, preferIndex bool) ([]byte, ocispecs.Descriptor, map[digest.Digest]*Source, error) {
	eg, ctx := errgroup.WithContext(ctx)

	dts := make([][]byte, len(srcs))
	for i := range dts {
		func(i int) {
			eg.Go(func() error {
				dt, err := r.GetDescriptor(ctx, srcs[i].Ref, srcs[i].Desc)
				if err != nil {
					return err
				}
				dts[i] = dt

				if srcs[i].Desc.MediaType == "" {
					mt, err := detectMediaType(dt)
					if err != nil {
						return err
					}
					srcs[i].Desc.MediaType = mt
				}

				mt := srcs[i].Desc.MediaType

				switch mt {
				case images.MediaTypeDockerSchema2Manifest, ocispecs.MediaTypeImageManifest:
					p := srcs[i].Desc.Platform
					if srcs[i].Desc.Platform == nil {
						p = &ocispecs.Platform{}
					}
					if p.OS == "" || p.Architecture == "" {
						if err := r.loadPlatform(ctx, srcs[i].Ref, p, dt); err != nil {
							return err
						}
					}
					srcs[i].Desc.Platform = p
				case images.MediaTypeDockerSchema1Manifest:
					return errors.Errorf("schema1 manifests are not allowed in manifest lists")
				}

				return nil
			})
		}(i)
	}

	if err := eg.Wait(); err != nil {
		return nil, ocispecs.Descriptor{}, nil, err
	}

	// on single source, return original bytes
	if len(srcs) == 1 && len(ann) == 0 {
		switch srcs[0].Desc.MediaType {
		// if the source is already an image index or manifest list, there is no need to consider the value
		// of preferIndex since if set to true then the source is already in the preferred format, and if false
		// it doesn't matter since we're not going to split it into separate manifests
		case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex:
			srcMap := map[digest.Digest]*Source{
				srcs[0].Desc.Digest: srcs[0],
			}
			return dts[0], srcs[0].Desc, srcMap, nil
		default:
			if !preferIndex {
				return dts[0], srcs[0].Desc, nil, nil
			}
		}
	}

	indexes := map[digest.Digest]int{}
	sources := map[digest.Digest]*Source{}
	descs := make([]ocispecs.Descriptor, 0, len(srcs))

	addDesc := func(d ocispecs.Descriptor, src *Source) {
		idx, ok := indexes[d.Digest]
		if ok {
			old := descs[idx]
			if old.MediaType == "" {
				old.MediaType = d.MediaType
			}
			if d.Platform != nil {
				old.Platform = d.Platform
			}
			if old.Annotations == nil {
				old.Annotations = map[string]string{}
			}
			maps.Copy(old.Annotations, d.Annotations)
			descs[idx] = old
		} else {
			indexes[d.Digest] = len(descs)
			descs = append(descs, d)
		}
		sources[d.Digest] = src
	}

	for i, src := range srcs {
		switch src.Desc.MediaType {
		case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex:
			var mfst ocispecs.Index
			if err := json.Unmarshal(dts[i], &mfst); err != nil {
				return nil, ocispecs.Descriptor{}, nil, errors.WithStack(err)
			}
			for _, d := range mfst.Manifests {
				addDesc(d, src)
			}
		default:
			addDesc(src.Desc, src)
		}
	}

	dockerMfsts := 0
	for _, desc := range descs {
		if strings.HasPrefix(desc.MediaType, "application/vnd.docker.") {
			dockerMfsts++
		}
	}

	var mt string
	if dockerMfsts == len(descs) {
		// all manifests are Docker types, use Docker manifest list
		mt = images.MediaTypeDockerSchema2ManifestList
	} else {
		// otherwise, use OCI index
		mt = ocispecs.MediaTypeImageIndex
	}

	// annotations are only allowed on OCI indexes
	indexAnnotation := make(map[string]string)
	if mt == ocispecs.MediaTypeImageIndex {
		for k, v := range ann {
			switch k.Type {
			case exptypes.AnnotationIndex:
				indexAnnotation[k.Key] = v
			case exptypes.AnnotationManifestDescriptor:
				for i := range descs {
					if descs[i].Annotations == nil {
						descs[i].Annotations = map[string]string{}
					}
					if k.Platform == nil || k.PlatformString() == platforms.Format(*descs[i].Platform) {
						descs[i].Annotations[k.Key] = v
					}
				}
			case exptypes.AnnotationManifest, "":
				return nil, ocispecs.Descriptor{}, nil, errors.Errorf("%q annotations are not supported yet", k.Type)
			case exptypes.AnnotationIndexDescriptor:
				return nil, ocispecs.Descriptor{}, nil, errors.Errorf("%q annotations are invalid while creating an image", k.Type)
			}
		}
	}

	idxBytes, err := json.MarshalIndent(ocispecs.Index{
		MediaType: mt,
		Versioned: specs.Versioned{
			SchemaVersion: 2,
		},
		Manifests:   descs,
		Annotations: indexAnnotation,
	}, "", "  ")
	if err != nil {
		return nil, ocispecs.Descriptor{}, nil, errors.Wrap(err, "failed to marshal index")
	}

	return idxBytes, ocispecs.Descriptor{
		MediaType: mt,
		Size:      int64(len(idxBytes)),
		Digest:    digest.FromBytes(idxBytes),
	}, sources, nil
}

func (r *Resolver) Push(ctx context.Context, ref *Location, desc ocispecs.Descriptor, dt []byte) error {
	ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto")
	if ref.IsOCILayout() {
		if err := ref.ValidateTargetDigest(desc.Digest); err != nil {
			return err
		}
		return r.pushOCILayout(ctx, ref, desc, dt)
	}

	fullRef, err := ref.WithDigest(desc.Digest)
	if err != nil {
		return errors.Wrapf(err, "failed to combine ref %s with digest %s", ref, desc.Digest)
	}
	p, err := r.registryResolver().Pusher(ctx, fullRef.String())
	if err != nil {
		return err
	}
	cw, err := p.Push(ctx, desc)
	if err != nil {
		if errdefs.IsAlreadyExists(err) {
			return nil
		}
		return err
	}

	err = content.Copy(ctx, cw, bytes.NewReader(dt), desc.Size, desc.Digest)
	if errdefs.IsAlreadyExists(err) {
		return nil
	}
	return err
}

func (r *Resolver) Copy(ctx context.Context, src *Source, dest *Location) error {
	ingester, err := r.IngesterForLocation(ctx, dest)
	if err != nil {
		return err
	}
	return r.CopyWithIngester(ctx, src, dest, ingester)
}

// CopyWithIngester copies a source manifest and its referrers to the
// destination using the provided ingester. Callers that issue multiple
// concurrent copies to the same destination should share a single ingester
// so that the underlying per-digest locking prevents duplicate blob pushes
// from racing against each other.
func (r *Resolver) CopyWithIngester(ctx context.Context, src *Source, dest *Location, ingester content.Ingester) error {
	ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto")
	ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.oci.empty.v1+json", "empty")

	desc := src.Desc
	desc.Annotations = maps.Clone(desc.Annotations)
	if desc.Annotations == nil {
		desc.Annotations = make(map[string]string)
	}
	if src.Ref.IsRegistry() {
		desc.Annotations["containerd.io/distribution.source."+src.Ref.Named().Name()] = src.Ref.Named().Name()
	}

	provider, err := r.providerForLocation(src.Ref)
	if err != nil {
		return err
	}

	referrers := &referrersProvider{base: referrersFunc(func(ctx context.Context, subject ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
		descs, err := r.FetchReferrers(ctx, src.Ref, subject.Digest)
		if err != nil {
			return nil, err
		}
		var filtered []ocispecs.Descriptor
		for _, d := range descs {
			if _, ok := supportedArtifactTypes[d.ArtifactType]; ok {
				filtered = append(filtered, d)
			}
		}
		return filtered, nil
	})}

	err = contentutil.CopyChain(ctx, ingester, provider, desc, contentutil.WithReferrers(referrers))
	if err != nil {
		return err
	}
	if dest.IsOCILayout() {
		for subject, descs := range referrers.refs {
			r.ociReferrers.record(dest.OCILayout().Path, subject, descs)
		}
	}
	return nil
}

func (r *Resolver) loadPlatform(ctx context.Context, loc *Location, p2 *ocispecs.Platform, dt []byte) error {
	var manifest ocispecs.Manifest
	if err := json.Unmarshal(dt, &manifest); err != nil {
		return errors.WithStack(err)
	}

	dt, err := r.GetDescriptor(ctx, loc, manifest.Config)
	if err != nil {
		return err
	}

	var p ocispecs.Platform
	if err := json.Unmarshal(dt, &p); err != nil {
		return errors.WithStack(err)
	}

	p = platforms.Normalize(p)

	if p2.Architecture == "" {
		p2.Architecture = p.Architecture
		if p2.Variant == "" {
			p2.Variant = p.Variant
		}
	}
	if p2.OS == "" {
		p2.OS = p.OS
	}

	return nil
}

type referrersFunc func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error)

func (f referrersFunc) Referrers(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
	return f(ctx, desc)
}

type DescWithSource struct {
	ocispecs.Descriptor
	Source *Source
}

func (r *Resolver) filterPlatforms(ctx context.Context, dt []byte, desc ocispecs.Descriptor, srcMap map[digest.Digest]*Source, plats []ocispecs.Platform) ([]byte, ocispecs.Descriptor, []DescWithSource, error) {
	matcher := platforms.Any(plats...)
	if len(plats) == 0 {
		matcher = platforms.All
	}

	if !images.IsIndexType(desc.MediaType) {
		var mfst ocispecs.Manifest
		if err := json.Unmarshal(dt, &mfst); err != nil {
			return nil, ocispecs.Descriptor{}, nil, errors.Wrapf(err, "failed to parse manifest")
		}
		if desc.Platform == nil {
			return nil, ocispecs.Descriptor{}, nil, errors.Errorf("cannot filter platforms from a manifest without platform information")
		}
		if !matcher.Match(*desc.Platform) {
			return nil, ocispecs.Descriptor{}, nil, errors.Errorf("input platform %s does not match any of the provided platforms", platforms.Format(*desc.Platform))
		}
		return dt, desc, nil, nil
	}

	var idx ocispecs.Index
	if err := json.Unmarshal(dt, &idx); err != nil {
		return nil, ocispecs.Descriptor{}, nil, errors.Wrapf(err, "failed to parse index")
	}

	var manifestMap = map[digest.Digest]ocispecs.Descriptor{}
	for _, m := range idx.Manifests {
		manifestMap[m.Digest] = m
	}
	var references = map[digest.Digest]ocispecs.Descriptor{}
	var matchedManifests = map[digest.Digest]struct{}{}
	for _, m := range idx.Manifests {
		if m.Platform == nil || matcher.Match(*m.Platform) {
			matchedManifests[m.Digest] = struct{}{}
		}
		if refType, ok := m.Annotations[attestation.DockerAnnotationReferenceType]; ok && refType == attestation.DockerAnnotationReferenceTypeDefault {
			dgstStr, ok := m.Annotations[attestation.DockerAnnotationReferenceDigest]
			if !ok {
				continue
			}
			dgst, err := digest.Parse(dgstStr)
			if err != nil {
				continue
			}
			subject, ok := manifestMap[dgst]
			if !ok {
				continue
			}
			if subject.Platform == nil || matcher.Match(*subject.Platform) {
				references[m.Digest] = subject
			}
		}
	}

	var mfsts []ocispecs.Descriptor
	var mfstsWithSource []DescWithSource

	for _, m := range idx.Manifests {
		_, isRef := references[m.Digest]
		if isRef || m.Platform == nil || matcher.Match(*m.Platform) {
			src, ok := srcMap[m.Digest]
			if !ok {
				defaultSource, ok := srcMap[desc.Digest]
				if !ok {
					return nil, ocispecs.Descriptor{}, nil, errors.Errorf("internal error: no source found for %s", m.Digest)
				}
				src = defaultSource
			}
			mfsts = append(mfsts, m)
			mfstsWithSource = append(mfstsWithSource, DescWithSource{
				Descriptor: m,
				Source:     src,
			})
		}
	}

	if len(mfsts) == 0 {
		return nil, ocispecs.Descriptor{}, nil, errors.Errorf("none of the manifests match the provided platforms")
	}

	// try to pull in attestation manifest via referrer if one exists
	addedRef := false
	for d := range matchedManifests {
		if _, ok := references[d]; ok { // manifest itself is already attestation
			continue
		}
		hasRef := false
		for _, subject := range references {
			if subject.Digest == d {
				hasRef = true
				break
			}
		}
		if hasRef {
			continue
		}
		src, ok := srcMap[d]
		if !ok {
			defaultSource, ok := srcMap[desc.Digest]
			if !ok {
				return nil, ocispecs.Descriptor{}, nil, errors.Errorf("internal error: no source found for %s", d)
			}
			src = defaultSource
		}
		refs, err := r.FetchReferrers(ctx, src.Ref, d, remotes.WithReferrerArtifactTypes(artifactTypeAttestationManifest))
		if err != nil {
			if errors.Is(err, errdefs.ErrNotFound) {
				continue
			}
			return nil, ocispecs.Descriptor{}, nil, err
		}
		for _, ref := range refs {
			if _, ok := references[ref.Digest]; ok {
				continue
			}
			ref.Platform = &ocispecs.Platform{
				OS: "unknown", Architecture: "unknown",
			}
			if ref.Annotations == nil {
				ref.Annotations = map[string]string{}
			}
			ref.Annotations[attestation.DockerAnnotationReferenceType] = attestation.DockerAnnotationReferenceTypeDefault
			ref.Annotations[attestation.DockerAnnotationReferenceDigest] = d.String()
			ref.ArtifactType = ""
			mfsts = append(mfsts, ref)
			addedRef = true
			break
		}
	}

	if len(mfsts) == len(idx.Manifests) && !addedRef {
		// all platforms matched, no need to rewrite index
		return dt, desc, mfstsWithSource, nil
	}

	idx.Manifests = mfsts
	idxBytes, err := json.MarshalIndent(&idx, "", "  ")
	if err != nil {
		return nil, ocispecs.Descriptor{}, nil, errors.Wrap(err, "failed to marshal index")
	}

	desc = ocispecs.Descriptor{
		MediaType:   desc.MediaType,
		Size:        int64(len(idxBytes)),
		Digest:      digest.FromBytes(idxBytes),
		Annotations: desc.Annotations,
	}

	return idxBytes, desc, mfstsWithSource, nil
}

type referrersProvider struct {
	base referrersFunc
	refs map[digest.Digest][]ocispecs.Descriptor
}

func (r *referrersProvider) Referrers(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
	out, err := r.base(ctx, desc)
	if err != nil {
		return nil, err
	}
	out = dedupeDescriptors(out)
	if r.refs == nil {
		r.refs = map[digest.Digest][]ocispecs.Descriptor{}
	}
	r.refs[desc.Digest] = dedupeDescriptors(append(r.refs[desc.Digest], out...))
	return out, nil
}

func dedupeDescriptors(descs []ocispecs.Descriptor) []ocispecs.Descriptor {
	if len(descs) < 2 {
		return descs
	}
	seen := make(map[digest.Digest]struct{}, len(descs))
	out := descs[:0]
	for _, desc := range descs {
		if _, ok := seen[desc.Digest]; ok {
			continue
		}
		seen[desc.Digest] = struct{}{}
		out = append(out, desc)
	}
	return out
}

// IngesterForLocation returns a content ingester for the given location.
// For registry locations a new pusher is created; for OCI layout locations
// the local content store is returned.
func (r *Resolver) IngesterForLocation(ctx context.Context, loc *Location) (content.Ingester, error) {
	if loc.IsRegistry() {
		p, err := r.registryResolver().Pusher(ctx, loc.Name())
		if err != nil {
			return nil, err
		}
		return contentutil.FromPusher(p), nil
	}
	return r.localStore(loc.OCILayout().Path)
}

func (r *Resolver) providerForLocation(loc *Location) (content.Provider, error) {
	if loc.IsRegistry() {
		f, err := r.registryResolver().Fetcher(context.TODO(), loc.String())
		if err != nil {
			return nil, err
		}
		return contentutil.FromFetcher(f), nil
	}
	return r.localStore(loc.OCILayout().Path)
}

func (r *Resolver) pushOCILayout(ctx context.Context, ref *Location, desc ocispecs.Descriptor, dt []byte) error {
	if err := os.MkdirAll(ref.OCILayout().Path, 0o755); err != nil {
		return err
	}
	store, err := r.localStore(ref.OCILayout().Path)
	if err != nil {
		return err
	}
	w, err := store.Writer(ctx, content.WithRef(desc.Digest.String()), content.WithDescriptor(desc))
	if err != nil {
		if !errdefs.IsAlreadyExists(err) {
			return err
		}
	} else {
		err = content.Copy(ctx, w, bytes.NewReader(dt), desc.Size, desc.Digest)
		if err != nil && !errdefs.IsAlreadyExists(err) {
			return err
		}
	}

	idx := ociindex.NewStoreIndex(ref.OCILayout().Path)
	switch {
	case ref.Digest() != "":
		if err := idx.Put(desc); err != nil {
			return err
		}
	case ref.Tag() != "":
		if err := idx.Put(desc, ociindex.Tag(ref.Tag())); err != nil {
			return err
		}
	default:
		if err := idx.Put(desc, ociindex.Tag("latest")); err != nil {
			return err
		}
	}
	return writePendingOCILayoutReferrers(ctx, r.ociReferrers.take(ref.OCILayout().Path), r.GetDescriptor, idx, ref)
}

func detectMediaType(dt []byte) (string, error) {
	var mfst struct {
		MediaType string          `json:"mediaType"`
		Config    json.RawMessage `json:"config"`
		FSLayers  []string        `json:"fsLayers"`
	}

	if err := json.Unmarshal(dt, &mfst); err != nil {
		return "", errors.WithStack(err)
	}

	if mfst.MediaType != "" {
		return mfst.MediaType, nil
	}
	if mfst.Config != nil {
		return images.MediaTypeDockerSchema2Manifest, nil
	}
	if len(mfst.FSLayers) > 0 {
		return images.MediaTypeDockerSchema1Manifest, nil
	}

	return images.MediaTypeDockerSchema2ManifestList, nil
}
