//go:build !remote && (linux || freebsd)

package libpod

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

	"go.podman.io/podman/v6/libpod/define"
	"go.podman.io/podman/v6/libpod/events"
	"go.podman.io/podman/v6/pkg/domain/entities/reports"
)

// Contains the public Runtime API for volumes

// A VolumeCreateOption is a functional option which alters the Volume created by
// NewVolume
type VolumeCreateOption func(*Volume) error

// VolumeFilter is a function to determine whether a volume is included in command
// output. Volumes to be outputted are tested using the function. a true return will
// include the volume, a false return will exclude it.
type VolumeFilter func(*Volume) bool

// RemoveVolume removes a volumes
func (r *Runtime) RemoveVolume(ctx context.Context, v *Volume, force bool, timeout *uint) error {
	if !r.valid {
		return define.ErrRuntimeStopped
	}

	return r.removeVolume(ctx, v, force, timeout, false)
}

// GetVolume retrieves a volume given its full name.
func (r *Runtime) GetVolume(name string) (*Volume, error) {
	if !r.valid {
		return nil, define.ErrRuntimeStopped
	}

	vol, err := r.state.Volume(name)
	if err != nil {
		return nil, err
	}

	return vol, nil
}

// LookupVolume retrieves a volume by unambiguous partial name.
func (r *Runtime) LookupVolume(name string) (*Volume, error) {
	if !r.valid {
		return nil, define.ErrRuntimeStopped
	}

	vol, err := r.state.LookupVolume(name)
	if err != nil {
		return nil, err
	}

	return vol, nil
}

// HasVolume checks to see if a volume with the given name exists
func (r *Runtime) HasVolume(name string) (bool, error) {
	if !r.valid {
		return false, define.ErrRuntimeStopped
	}

	return r.state.HasVolume(name)
}

// Volumes retrieves all volumes
// Filters can be provided which will determine which volumes are included in the
// output. If multiple filters are used, a volume will be returned if
// all of the filters are matched
func (r *Runtime) Volumes(filters ...VolumeFilter) ([]*Volume, error) {
	if !r.valid {
		return nil, define.ErrRuntimeStopped
	}

	vols, err := r.state.AllVolumes()
	if err != nil {
		return nil, err
	}

	if len(filters) == 0 {
		return vols, nil
	}

	volsFiltered := make([]*Volume, 0, len(vols))
	for _, vol := range vols {
		include := true
		for _, filter := range filters {
			include = include && filter(vol)
		}

		if include {
			volsFiltered = append(volsFiltered, vol)
		}
	}

	return volsFiltered, nil
}

// GetAllVolumes retrieves all the volumes
func (r *Runtime) GetAllVolumes() ([]*Volume, error) {
	if !r.valid {
		return nil, define.ErrRuntimeStopped
	}

	return r.state.AllVolumes()
}

// RenameVolume renames the given volume to a new name.
// The volume must not be in use by any containers, and must use the local
// driver.
func (r *Runtime) RenameVolume(_ context.Context, vol *Volume, newName string) (*Volume, error) {
	if !r.valid {
		return nil, define.ErrRuntimeStopped
	}

	vol.lock.Lock()
	defer vol.lock.Unlock()

	if err := vol.update(); err != nil {
		return nil, err
	}

	if newName == "" || !define.NameRegex.MatchString(newName) {
		return nil, define.RegexError
	}

	if vol.Name() == newName {
		return vol, nil
	}

	// Only local-driver volumes can be renamed
	driver := vol.Driver()
	if driver != "" && driver != define.VolumeDriverLocal {
		return nil, fmt.Errorf("renaming volume %s: rename is not supported for volumes using driver %q: %w", vol.Name(), driver, define.ErrInvalidArg)
	}

	// Refuse rename if the volume is currently mounted
	if vol.state.MountCount > 0 {
		return nil, fmt.Errorf("renaming volume %s: volume is currently mounted: %w", vol.Name(), define.ErrVolumeBeingUsed)
	}

	// Refuse rename if the volume is in use by any container
	ctrs, err := r.state.VolumeInUse(vol)
	if err != nil {
		return nil, fmt.Errorf("checking if volume %s is in use: %w", vol.Name(), err)
	}
	if len(ctrs) > 0 {
		return nil, fmt.Errorf("volume %s is being used by the following container(s): %s: %w", vol.Name(), strings.Join(ctrs, ", "), define.ErrVolumeBeingUsed)
	}

	oldName := vol.config.Name
	config := *vol.config
	config.Name = newName
	config.MountPoint = r.volumeDataPath(newName)
	config.IsAnon = false

	if err := r.state.RenameVolume(vol, &config); err != nil {
		return nil, fmt.Errorf("renaming volume %s: %w", oldName, err)
	}
	vol.config = &config

	vol.newVolumeEvent(events.Rename)
	return vol, nil
}

// PruneVolumes removes unused volumes from the system
func (r *Runtime) PruneVolumes(ctx context.Context, filterFuncs []VolumeFilter, dryRun bool) ([]*reports.PruneReport, error) {
	preports := make([]*reports.PruneReport, 0)
	vols, err := r.Volumes(filterFuncs...)
	if err != nil {
		return nil, err
	}

	for _, vol := range vols {
		dangling, err := vol.IsDangling()
		if err != nil {
			preports = append(preports, &reports.PruneReport{
				Id:  vol.Name(),
				Err: err,
			})
		}
		if !dangling {
			continue
		}

		report := new(reports.PruneReport)
		volSize, err := vol.Size()
		if err != nil {
			volSize = 0
		}
		report.Size = volSize
		report.Id = vol.Name()
		if !dryRun {
			var timeout *uint
			if err := r.RemoveVolume(ctx, vol, false, timeout); err != nil {
				if !errors.Is(err, define.ErrVolumeBeingUsed) && !errors.Is(err, define.ErrVolumeRemoved) {
					report.Err = err
				} else {
					// We didn't remove the volume for some reason
					continue
				}
			} else {
				vol.newVolumeEvent(events.Prune)
			}
		}
		preports = append(preports, report)
	}
	return preports, nil
}
