package image

import (
	"context"
	"strings"
	"time"

	"github.com/containers/podman/v3/libpod/events"
	"github.com/containers/podman/v3/pkg/domain/entities/reports"
	"github.com/containers/podman/v3/pkg/timetype"
	"github.com/containers/storage"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

func generatePruneFilterFuncs(filter, filterValue string) (ImageFilter, error) {
	switch filter {
	case "label":
		var filterArray = strings.SplitN(filterValue, "=", 2)
		var filterKey = filterArray[0]
		if len(filterArray) > 1 {
			filterValue = filterArray[1]
		} else {
			filterValue = ""
		}
		return func(i *Image) bool {
			labels, err := i.Labels(context.Background())
			if err != nil {
				return false
			}
			for labelKey, labelValue := range labels {
				if labelKey == filterKey && (filterValue == "" || labelValue == filterValue) {
					return true
				}
			}
			return false
		}, nil

	case "until":
		ts, err := timetype.GetTimestamp(filterValue, time.Now())
		if err != nil {
			return nil, err
		}
		seconds, nanoseconds, err := timetype.ParseTimestamps(ts, 0)
		if err != nil {
			return nil, err
		}
		until := time.Unix(seconds, nanoseconds)
		return func(i *Image) bool {
			if !until.IsZero() && i.Created().After((until)) {
				return true
			}
			return false
		}, nil
	}
	return nil, nil
}

// GetPruneImages returns a slice of images that have no names/unused
func (ir *Runtime) GetPruneImages(ctx context.Context, all bool, filterFuncs []ImageFilter) ([]*Image, error) {
	var (
		pruneImages []*Image
	)

	allImages, err := ir.GetRWImages()
	if err != nil {
		return nil, err
	}

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

	for _, i := range allImages {
		// filter the images based on this.
		for _, filterFunc := range filterFuncs {
			if !filterFunc(i) {
				continue
			}
		}

		if all {
			containers, err := i.Containers()
			if err != nil {
				return nil, err
			}
			if len(containers) < 1 {
				pruneImages = append(pruneImages, i)
				continue
			}
		}

		// skip the cache (i.e., with parent) and intermediate (i.e.,
		// with children) images
		intermediate, err := tree.hasChildrenAndParent(ctx, i)
		if err != nil {
			return nil, err
		}
		if intermediate {
			continue
		}

		if i.Dangling() {
			pruneImages = append(pruneImages, i)
		}
	}
	return pruneImages, nil
}

// PruneImages prunes dangling and optionally all unused images from the local
// image store
func (ir *Runtime) PruneImages(ctx context.Context, all bool, filter []string) ([]*reports.PruneReport, error) {
	preports := make([]*reports.PruneReport, 0)
	filterFuncs := make([]ImageFilter, 0, len(filter))
	for _, f := range filter {
		filterSplit := strings.SplitN(f, "=", 2)
		if len(filterSplit) < 2 {
			return nil, errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
		}

		generatedFunc, err := generatePruneFilterFuncs(filterSplit[0], filterSplit[1])
		if err != nil {
			return nil, errors.Wrapf(err, "invalid filter")
		}
		filterFuncs = append(filterFuncs, generatedFunc)
	}

	prev := 0
	for {
		toPrune, err := ir.GetPruneImages(ctx, all, filterFuncs)
		if err != nil {
			return nil, errors.Wrap(err, "unable to get images to prune")
		}
		numImages := len(toPrune)
		if numImages == 0 || numImages == prev {
			// If there's nothing left to do, return.
			break
		}
		prev = numImages
		for _, img := range toPrune {
			repotags, err := img.RepoTags()
			if err != nil {
				return nil, err
			}
			nameOrID := img.ID()
			s, err := img.Size(ctx)
			imgSize := *s
			if err != nil {
				logrus.Warnf("Failed to collect image size for: %s, %s", nameOrID, err)
				imgSize = 0
			}
			if err := img.Remove(ctx, false); err != nil {
				if errors.Cause(err) == storage.ErrImageUsedByContainer {
					logrus.Warnf("Failed to prune image %s as it is in use: %v.\nA container associated with containers/storage (e.g., Buildah, CRI-O, etc.) maybe associated with this image.\nUsing the rmi command with the --force option will remove the container and image, but may cause failures for other dependent systems.", img.ID(), err)
					continue
				}
				return nil, errors.Wrap(err, "failed to prune image")
			}
			defer img.newImageEvent(events.Prune)

			if len(repotags) > 0 {
				nameOrID = repotags[0]
			}

			preports = append(preports, &reports.PruneReport{
				Id:   nameOrID,
				Err:  nil,
				Size: uint64(imgSize),
			})
		}
	}
	return preports, nil
}
