package server

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"

	json "github.com/goccy/go-json"
	specs "github.com/opencontainers/image-spec/specs-go/v1"
	istorage "go.podman.io/image/v5/storage"
	"go.podman.io/storage"
	types "k8s.io/cri-api/pkg/apis/runtime/v1"

	"github.com/cri-o/cri-o/internal/log"
	"github.com/cri-o/cri-o/internal/ociartifact"
	pkgstorage "github.com/cri-o/cri-o/internal/storage"
)

// ImageStatus returns the status of the image.
func (s *Server) ImageStatus(ctx context.Context, req *types.ImageStatusRequest) (*types.ImageStatusResponse, error) {
	ctx, span := log.StartSpan(ctx)
	defer span.End()

	img := req.GetImage()
	if img == nil || img.GetImage() == "" {
		return nil, errors.New("no image specified")
	}

	log.Infof(ctx, "Checking image status: %s", img.GetImage())

	status, err := s.storageImageStatus(ctx, img)
	if err != nil {
		return nil, err
	}

	if status == nil {
		artifact, err := s.ArtifactStore().Status(ctx, img.GetImage())
		if err == nil {
			return &types.ImageStatusResponse{
				Image: artifact.CRIImage(),
			}, nil
		}

		if errors.Is(err, ociartifact.ErrNotFound) {
			log.Infof(ctx, "Neither image nor artfiact %s found", img.GetImage())
		} else if err != nil {
			log.Errorf(ctx, "Unable to get artifact: %v", err)
		}

		return &types.ImageStatusResponse{}, nil
	}

	// Ensure that size is already defined
	var size uint64
	if status.Size == nil {
		size = 0
	} else {
		size = *status.Size
	}

	resp := &types.ImageStatusResponse{
		Image: &types.Image{
			Id:          status.ID.IDStringForOutOfProcessConsumptionOnly(),
			RepoTags:    status.RepoTags,
			RepoDigests: status.RepoDigests,
			Size:        size,
			Spec: &types.ImageSpec{
				Annotations: status.Annotations,
			},
			Pinned: status.Pinned,
		},
	}

	if req.GetVerbose() {
		info, err := createImageInfo(status)
		if err != nil {
			return nil, fmt.Errorf("creating image info: %w", err)
		}

		resp.Info = info
	}

	uid, username := getUserFromImage(status.User)
	if uid != nil {
		resp.Image.Uid = &types.Int64Value{Value: *uid}
	}

	resp.Image.Username = username

	return resp, nil
}

// storageImageStatus calls ImageStatus for a k8s ImageSpec.
// Returns (nil, nil) if image was not found.
func (s *Server) storageImageStatus(ctx context.Context, spec *types.ImageSpec) (*pkgstorage.ImageResult, error) {
	if id := s.ContainerServer.StorageImageServer().HeuristicallyTryResolvingStringAsIDPrefix(spec.GetImage()); id != nil {
		status, err := s.ContainerServer.StorageImageServer().ImageStatusByID(s.config.SystemContext, *id)
		if err != nil {
			if errors.Is(err, istorage.ErrNoSuchImage) || errors.Is(err, storage.ErrImageUnknown) {
				log.Infof(ctx, "Image %s not found", spec.GetImage())

				return nil, nil
			}

			log.Warnf(ctx, "Error getting status from %s: %v", spec.GetImage(), err)

			return nil, err
		}

		return status, nil
	}

	potentialMatches, err := s.ContainerServer.StorageImageServer().CandidatesForPotentiallyShortImageName(s.config.SystemContext, spec.GetImage())
	if err != nil {
		if len(spec.GetImage()) >= 3 && isHexString(spec.GetImage()) {
			log.Debugf(ctx, "CandidatesForPotentiallyShortImageName failed for %q, but input looks like digest/ID: %v", spec.GetImage(), err)

			return nil, nil
		}

		return nil, err
	}

	var lastErr error

	for _, name := range potentialMatches {
		status, err := s.ContainerServer.StorageImageServer().ImageStatusByName(s.config.SystemContext, name)
		if err != nil {
			if errors.Is(err, istorage.ErrNoSuchImage) {
				log.Debugf(ctx, "Can't find %s", name)

				continue
			}

			log.Warnf(ctx, "Error getting status from %s: %v", name, err)
			lastErr = err

			continue
		}

		return status, nil
	}

	if lastErr != nil {
		return nil, lastErr
	}
	// CandidatesForPotentiallyShortImageName returns at least one value if it doesn't fail.
	// So, if we got here, there was at least one ErrNoSuchImage, and no other errors.
	log.Infof(ctx, "Image %s not found", spec.GetImage())

	return nil, nil
}

// getUserFromImage gets uid or user name of the image user.
// If user is numeric, it will be treated as uid; or else, it is treated as user name.
func getUserFromImage(user string) (id *int64, username string) {
	// return both empty if user is not specified in the image.
	if user == "" {
		return nil, ""
	}
	// split instances where the id may contain user:group
	user = strings.Split(user, ":")[0]
	// user could be either uid or user name. Try to interpret as numeric uid.
	uid, err := strconv.ParseInt(user, 10, 64)
	if err != nil {
		// If user is non numeric, assume it's user name.
		return nil, user
	}
	// If user is a numeric uid.
	return &uid, ""
}

func createImageInfo(result *pkgstorage.ImageResult) (map[string]string, error) {
	info := struct {
		Labels    map[string]string `json:"labels,omitempty"`
		ImageSpec *specs.Image      `json:"imageSpec"`
	}{
		result.Labels,
		result.OCIConfig,
	}

	bytes, err := json.Marshal(info)
	if err != nil {
		return nil, fmt.Errorf("marshal data: %v: %w", info, err)
	}

	return map[string]string{"info": string(bytes)}, nil
}

// isHexString returns true if the string contains only hexadecimal characters.
func isHexString(s string) bool {
	for _, c := range s {
		if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
			return false
		}
	}

	return true
}
