package containerd

import (
	"context"
	"fmt"
	"sync/atomic"
	"time"

	c8dimages "github.com/containerd/containerd/v2/core/images"
	cerrdefs "github.com/containerd/errdefs"
	"github.com/containerd/log"
	"github.com/containerd/platforms"
	"github.com/distribution/reference"
	dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
	imagetypes "github.com/moby/moby/api/types/image"
	"github.com/moby/moby/api/types/storage"
	"github.com/moby/moby/v2/daemon/server/imagebackend"
	"github.com/moby/moby/v2/internal/sliceutil"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"golang.org/x/sync/semaphore"
)

func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, opts imagebackend.ImageInspectOpts) (*imagebackend.InspectData, error) {
	requestedPlatform := opts.Platform

	c8dImg, err := i.resolveImage(ctx, refOrID)
	if err != nil {
		return nil, err
	}

	target := c8dImg.Target
	tagged, err := i.images.List(ctx, "target.digest=="+target.Digest.String())
	if err != nil {
		return nil, err
	}

	// This could happen only if the image was deleted after the resolveImage call above.
	if len(tagged) == 0 {
		return nil, errInconsistentData
	}

	lastUpdated := time.Unix(0, 0)
	for _, i := range tagged {
		if i.UpdatedAt.After(lastUpdated) {
			lastUpdated = i.UpdatedAt
		}
	}

	platform := i.matchRequestedOrDefault(platforms.OnlyStrict, requestedPlatform)
	size, err := i.size(ctx, target, platform)
	if err != nil {
		return nil, err
	}

	multi, err := i.multiPlatformSummary(ctx, c8dImg, platform)
	if err != nil {
		return nil, err
	}

	if multi.Best == nil && requestedPlatform != nil {
		return nil, &errPlatformNotFound{
			imageRef: refOrID,
			wanted:   *requestedPlatform,
		}
	}

	parent, err := i.getImageLabelByDigest(ctx, target.Digest, imageLabelClassicBuilderParent)
	if err != nil {
		log.G(ctx).WithError(err).Warn("failed to determine Parent property")
	}

	var manifests []imagetypes.ManifestSummary
	if opts.Manifests {
		manifests = multi.Manifests
	}

	repoTags, repoDigests := collectRepoTagsAndDigests(ctx, tagged)

	if requestedPlatform != nil {
		target = multi.Best.Target()
	}

	var identity *imagetypes.Identity
	if opts.Identity {
		var err error
		identity, err = i.imageIdentity(ctx, c8dImg.Target, multi)
		if err != nil {
			log.G(ctx).WithError(err).Warn("failed to determine Identity property")
		}
	}

	resp := &imagebackend.InspectData{
		InspectResponse: imagetypes.InspectResponse{
			ID:          target.Digest.String(),
			RepoTags:    repoTags,
			Descriptor:  &target,
			RepoDigests: repoDigests,
			Size:        size,
			Manifests:   manifests,
			Metadata: imagetypes.Metadata{
				LastTagTime: lastUpdated,
			},
			Identity: identity,
		},
		Parent: parent, // field is deprecated with the legacy builder, but returned by the API if present.

		// GraphDriver is omitted in API v1.52 unless using a graphdriver.
		GraphDriverLegacy: &storage.DriverData{Name: i.snapshotter},
	}

	var img dockerspec.DockerOCIImage
	if multi.Best != nil {
		if err := multi.Best.ReadConfig(ctx, &img); err != nil && !cerrdefs.IsNotFound(err) {
			return nil, fmt.Errorf("failed to read image config: %w", err)
		}
	}

	// Copy the config
	imgConfig := img.Config
	resp.Config = &imgConfig

	resp.Author = img.Author
	resp.Architecture = img.Architecture
	resp.Variant = img.Variant
	resp.Os = img.OS
	resp.OsVersion = img.OSVersion

	if len(img.History) > 0 {
		resp.Comment = img.History[len(img.History)-1].Comment
	}

	if img.Created != nil {
		resp.Created = img.Created.Format(time.RFC3339Nano)
	}

	resp.RootFS = imagetypes.RootFS{
		Type: img.RootFS.Type,
	}
	for _, layer := range img.RootFS.DiffIDs {
		resp.RootFS.Layers = append(resp.RootFS.Layers, layer.String())
	}

	return resp, nil
}

func collectRepoTagsAndDigests(ctx context.Context, tagged []c8dimages.Image) (repoTags []string, repoDigests []string) {
	repoTags = make([]string, 0, len(tagged))
	repoDigests = make([]string, 0, len(tagged))
	for _, img := range tagged {
		if isDanglingImage(img) {
			if len(tagged) > 1 {
				// This is unexpected - dangling image should be deleted
				// as soon as another image with the same target is created.
				// Log a warning, but don't error out the whole operation.
				log.G(ctx).WithField("refs", tagged).Warn("multiple images have the same target, but one of them is still dangling")
			}
			continue
		}

		name, err := reference.ParseNamed(img.Name)
		if err != nil {
			log.G(ctx).WithField("name", name).WithError(err).Error("failed to parse image name as reference")
			// Include the malformed name in RepoTags to be consistent with `docker image ls`.
			repoTags = append(repoTags, img.Name)
			continue
		}

		repoTags = append(repoTags, reference.FamiliarString(name))
		if _, ok := name.(reference.Digested); ok {
			repoDigests = append(repoDigests, reference.FamiliarString(name))
			// Image name is a digested reference already, so no need to create a digested reference.
			continue
		}

		digested, err := reference.WithDigest(reference.TrimNamed(name), img.Target.Digest)
		if err != nil {
			// This could only happen if digest is invalid, but considering that
			// we get it from the Descriptor it's highly unlikely.
			// Log error just in case.
			log.G(ctx).WithError(err).Error("failed to create digested reference")
			continue
		}
		repoDigests = append(repoDigests, reference.FamiliarString(digested))
	}
	return sliceutil.Dedup(repoTags), sliceutil.Dedup(repoDigests)
}

// size returns the total size of the image's packed resources.
func (i *ImageService) size(ctx context.Context, desc ocispec.Descriptor, platform platforms.MatchComparer) (int64, error) {
	var size atomic.Int64

	cs := i.content
	handler := c8dimages.LimitManifests(c8dimages.ChildrenHandler(cs), platform, 1)

	var wh c8dimages.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
		children, err := handler(ctx, desc)
		if err != nil {
			if !cerrdefs.IsNotFound(err) {
				return nil, err
			}
		}

		size.Add(desc.Size)

		return children, nil
	}

	l := semaphore.NewWeighted(3)
	if err := c8dimages.Dispatch(ctx, wh, l, desc); err != nil {
		return 0, err
	}

	return size.Load(), nil
}
