package service

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/containerd/log"
	"github.com/docker/docker/api/types/filters"
	volumetypes "github.com/docker/docker/api/types/volume"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/internal/directory"
	"github.com/docker/docker/volume"
)

// convertOpts are used to pass options to `volumeToAPI`
type convertOpt interface {
	isConvertOpt()
}

type useCachedPath bool

func (useCachedPath) isConvertOpt() {}

type calcSize bool

func (calcSize) isConvertOpt() {}

type pathCacher interface {
	CachedPath() string
}

func (s *VolumesService) volumesToAPI(ctx context.Context, volumes []volume.Volume, opts ...convertOpt) []*volumetypes.Volume {
	var (
		out        = make([]*volumetypes.Volume, 0, len(volumes))
		getSize    bool
		cachedPath bool
	)

	for _, o := range opts {
		switch t := o.(type) {
		case calcSize:
			getSize = bool(t)
		case useCachedPath:
			cachedPath = bool(t)
		}
	}
	for _, v := range volumes {
		select {
		case <-ctx.Done():
			return nil
		default:
		}
		apiV := volumeToAPIType(v)

		if cachedPath {
			if vv, ok := v.(pathCacher); ok {
				apiV.Mountpoint = vv.CachedPath()
			}
		} else {
			apiV.Mountpoint = v.Path()
		}

		if getSize {
			p := v.Path()
			if apiV.Mountpoint == "" {
				apiV.Mountpoint = p
			}
			sz, err := directory.Size(ctx, p)
			if err != nil {
				log.G(ctx).WithError(err).WithField("volume", v.Name()).Warnf("Failed to determine size of volume")
				sz = -1
			}
			apiV.UsageData = &volumetypes.UsageData{Size: sz, RefCount: int64(s.vs.CountReferences(v))}
		}

		out = append(out, &apiV)
	}
	return out
}

func volumeToAPIType(v volume.Volume) volumetypes.Volume {
	createdAt, _ := v.CreatedAt()
	tv := volumetypes.Volume{
		Name:      v.Name(),
		Driver:    v.DriverName(),
		CreatedAt: createdAt.Format(time.RFC3339),
	}
	if v, ok := v.(volume.DetailedVolume); ok {
		tv.Labels = v.Labels()
		tv.Options = v.Options()
		tv.Scope = v.Scope()
	}
	if cp, ok := v.(pathCacher); ok {
		tv.Mountpoint = cp.CachedPath()
	}
	return tv
}

func filtersToBy(filter filters.Args, acceptedFilters map[string]bool) (By, error) {
	if err := filter.Validate(acceptedFilters); err != nil {
		return nil, err
	}
	var bys []By
	if drivers := filter.Get("driver"); len(drivers) > 0 {
		bys = append(bys, ByDriver(drivers...))
	}
	if filter.Contains("name") {
		bys = append(bys, CustomFilter(func(v volume.Volume) bool {
			return filter.Match("name", v.Name())
		}))
	}
	bys = append(bys, byLabelFilter(filter))

	if filter.Contains("dangling") {
		dangling, err := filter.GetBoolOrDefault("dangling", false)
		if err != nil {
			return nil, err
		}
		bys = append(bys, ByReferenced(!dangling))
	}

	var by By
	switch len(bys) {
	case 0:
	case 1:
		by = bys[0]
	default:
		by = And(bys...)
	}
	return by, nil
}

func withPrune(filter filters.Args) error {
	all := filter.Get("all")
	switch {
	case len(all) > 1:
		return errdefs.InvalidParameter(fmt.Errorf("invalid filter 'all=%s': only one value is expected", all))
	case len(all) == 1:
		ok, err := strconv.ParseBool(all[0])
		if err != nil {
			return errdefs.InvalidParameter(fmt.Errorf("invalid filter 'all': %w", err))
		}
		if ok {
			return nil
		}
	}

	filter.Add("label", AnonymousLabel)
	return nil
}
