package abi

import (
	"context"
	"fmt"
	"io/ioutil"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/containers/common/pkg/config"
	"github.com/containers/podman/v3/libpod"
	"github.com/containers/podman/v3/libpod/define"
	"github.com/containers/podman/v3/pkg/cgroups"
	"github.com/containers/podman/v3/pkg/domain/entities"
	"github.com/containers/podman/v3/pkg/domain/entities/reports"
	"github.com/containers/podman/v3/pkg/rootless"
	"github.com/containers/podman/v3/pkg/util"
	"github.com/containers/podman/v3/utils"
	"github.com/containers/storage"
	"github.com/containers/storage/pkg/unshare"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
)

func (ic *ContainerEngine) Info(ctx context.Context) (*define.Info, error) {
	info, err := ic.Libpod.Info()
	if err != nil {
		return nil, err
	}

	socketPath, err := util.SocketPath()
	if err != nil {
		return nil, err
	}
	rs := define.RemoteSocket{
		Path:   socketPath,
		Exists: false,
	}

	// Check if the socket exists
	if fi, err := os.Stat(socketPath); err == nil {
		if fi.Mode()&os.ModeSocket != 0 {
			rs.Exists = true
		}
	}
	// TODO
	// it was suggested future versions of this could perform
	// a ping on the socket for greater confidence the socket is
	// actually active.
	info.Host.RemoteSocket = &rs
	return info, err
}

func (ic *ContainerEngine) SetupRootless(_ context.Context, cmd *cobra.Command) error {
	// do it only after podman has already re-execed and running with uid==0.
	hasCapSysAdmin, err := unshare.HasCapSysAdmin()
	if err != nil {
		return err
	}
	if hasCapSysAdmin {
		ownsCgroup, err := cgroups.UserOwnsCurrentSystemdCgroup()
		if err != nil {
			logrus.Infof("Failed to detect the owner for the current cgroup: %v", err)
		}
		if !ownsCgroup {
			conf, err := ic.Config(context.Background())
			if err != nil {
				return err
			}

			initCommand, err := ioutil.ReadFile("/proc/1/comm")
			// On errors, default to systemd
			runsUnderSystemd := err != nil || strings.TrimRight(string(initCommand), "\n") == "systemd"

			unitName := fmt.Sprintf("podman-%d.scope", os.Getpid())
			if runsUnderSystemd || conf.Engine.CgroupManager == config.SystemdCgroupsManager {
				if err := utils.RunUnderSystemdScope(os.Getpid(), "user.slice", unitName); err != nil {
					logrus.Debugf("Failed to add podman to systemd sandbox cgroup: %v", err)
				}
			}
		}
		return nil
	}

	tmpDir, err := ic.Libpod.TmpDir()
	if err != nil {
		return err
	}
	pausePidPath, err := util.GetRootlessPauseProcessPidPathGivenDir(tmpDir)
	if err != nil {
		return errors.Wrapf(err, "could not get pause process pid file path")
	}

	became, ret, err := rootless.TryJoinPauseProcess(pausePidPath)
	if err != nil {
		return err
	}
	if became {
		os.Exit(ret)
	}

	// if there is no pid file, try to join existing containers, and create a pause process.
	ctrs, err := ic.Libpod.GetRunningContainers()
	if err != nil {
		logrus.Error(err.Error())
		os.Exit(1)
	}

	paths := []string{}
	for _, ctr := range ctrs {
		paths = append(paths, ctr.Config().ConmonPidFile)
	}

	became, ret, err = rootless.TryJoinFromFilePaths(pausePidPath, true, paths)
	if err := movePauseProcessToScope(ic.Libpod); err != nil {
		conf, err := ic.Config(context.Background())
		if err != nil {
			return err
		}
		if conf.Engine.CgroupManager == config.SystemdCgroupsManager {
			logrus.Warnf("Failed to add pause process to systemd sandbox cgroup: %v", err)
		} else {
			logrus.Debugf("Failed to add pause process to systemd sandbox cgroup: %v", err)
		}
	}
	if err != nil {
		logrus.Error(errors.Wrapf(err, "invalid internal status, try resetting the pause process with %q", os.Args[0]+" system migrate"))
		os.Exit(1)
	}
	if became {
		os.Exit(ret)
	}
	return nil
}

func movePauseProcessToScope(r *libpod.Runtime) error {
	tmpDir, err := r.TmpDir()
	if err != nil {
		return err
	}
	pausePidPath, err := util.GetRootlessPauseProcessPidPathGivenDir(tmpDir)
	if err != nil {
		return errors.Wrapf(err, "could not get pause process pid file path")
	}

	data, err := ioutil.ReadFile(pausePidPath)
	if err != nil {
		return errors.Wrapf(err, "cannot read pause pid file")
	}
	pid, err := strconv.ParseUint(string(data), 10, 0)
	if err != nil {
		return errors.Wrapf(err, "cannot parse pid file %s", pausePidPath)
	}

	return utils.RunUnderSystemdScope(int(pid), "user.slice", "podman-pause.scope")
}

// SystemPrune removes unused data from the system. Pruning pods, containers, volumes and images.
func (ic *ContainerEngine) SystemPrune(ctx context.Context, options entities.SystemPruneOptions) (*entities.SystemPruneReport, error) {
	var systemPruneReport = new(entities.SystemPruneReport)
	filters := []string{}
	for k, v := range options.Filters {
		filters = append(filters, fmt.Sprintf("%s=%s", k, v[0]))
	}
	reclaimedSpace := (uint64)(0)
	found := true
	for found {
		found = false
		podPruneReport, err := ic.prunePodHelper(ctx)
		if err != nil {
			return nil, err
		}
		if len(podPruneReport) > 0 {
			found = true
		}
		systemPruneReport.PodPruneReport = append(systemPruneReport.PodPruneReport, podPruneReport...)

		// TODO: Figure out cleaner way to handle all of the different PruneOptions
		containerPruneOptions := entities.ContainerPruneOptions{}
		containerPruneOptions.Filters = (url.Values)(options.Filters)

		containerPruneReports, err := ic.ContainerPrune(ctx, containerPruneOptions)
		if err != nil {
			return nil, err
		}
		reclaimedSpace = reclaimedSpace + reports.PruneReportsSize(containerPruneReports)
		systemPruneReport.ContainerPruneReports = append(systemPruneReport.ContainerPruneReports, containerPruneReports...)
		imagePruneOptions := entities.ImagePruneOptions{
			All:    options.All,
			Filter: filters,
		}
		imageEngine := ImageEngine{Libpod: ic.Libpod}
		imagePruneReports, err := imageEngine.Prune(ctx, imagePruneOptions)
		reclaimedSpace = reclaimedSpace + reports.PruneReportsSize(imagePruneReports)

		if err != nil {
			return nil, err
		}
		if len(imagePruneReports) > 0 {
			found = true
		}

		systemPruneReport.ImagePruneReports = append(systemPruneReport.ImagePruneReports, imagePruneReports...)
		if options.Volume {
			volumePruneOptions := entities.VolumePruneOptions{}
			volumePruneOptions.Filters = (url.Values)(options.Filters)
			volumePruneReport, err := ic.VolumePrune(ctx, volumePruneOptions)
			if err != nil {
				return nil, err
			}
			if len(volumePruneReport) > 0 {
				found = true
			}
			reclaimedSpace = reclaimedSpace + reports.PruneReportsSize(volumePruneReport)
			systemPruneReport.VolumePruneReports = append(systemPruneReport.VolumePruneReports, volumePruneReport...)
		}
	}
	systemPruneReport.ReclaimedSpace = reclaimedSpace
	return systemPruneReport, nil
}

func (ic *ContainerEngine) SystemDf(ctx context.Context, options entities.SystemDfOptions) (*entities.SystemDfReport, error) {
	var (
		dfImages = []*entities.SystemDfImageReport{}
	)

	imageStats, err := ic.Libpod.LibimageRuntime().DiskUsage(ctx)
	if err != nil {
		return nil, err
	}

	for _, stat := range imageStats {
		report := entities.SystemDfImageReport{
			Repository: stat.Repository,
			Tag:        stat.Tag,
			ImageID:    stat.ID,
			Created:    stat.Created,
			Size:       int64(stat.Size),
			SharedSize: int64(stat.SharedSize),
			UniqueSize: int64(stat.UniqueSize),
			Containers: stat.Containers,
		}
		dfImages = append(dfImages, &report)
	}

	// Get Containers and iterate them
	cons, err := ic.Libpod.GetAllContainers()
	if err != nil {
		return nil, err
	}
	dfContainers := make([]*entities.SystemDfContainerReport, 0, len(cons))
	for _, c := range cons {
		iid, _ := c.Image()
		state, err := c.State()
		if err != nil {
			return nil, errors.Wrapf(err, "Failed to get state of container %s", c.ID())
		}
		conSize, err := c.RootFsSize()
		if err != nil {
			if errors.Cause(err) == storage.ErrContainerUnknown {
				logrus.Error(errors.Wrapf(err, "Failed to get root file system size of container %s", c.ID()))
			} else {
				return nil, errors.Wrapf(err, "Failed to get root file system size of container %s", c.ID())
			}
		}
		rwsize, err := c.RWSize()
		if err != nil {
			if errors.Cause(err) == storage.ErrContainerUnknown {
				logrus.Error(errors.Wrapf(err, "Failed to get read/write size of container %s", c.ID()))
			} else {
				return nil, errors.Wrapf(err, "Failed to get read/write size of container %s", c.ID())
			}
		}
		report := entities.SystemDfContainerReport{
			ContainerID:  c.ID(),
			Image:        iid,
			Command:      c.Command(),
			LocalVolumes: len(c.UserVolumes()),
			RWSize:       rwsize,
			Size:         conSize,
			Created:      c.CreatedTime(),
			Status:       state.String(),
			Names:        c.Name(),
		}
		dfContainers = append(dfContainers, &report)
	}

	//	Get volumes and iterate them
	vols, err := ic.Libpod.GetAllVolumes()
	if err != nil {
		return nil, err
	}

	running, err := ic.Libpod.GetRunningContainers()
	if err != nil {
		return nil, err
	}
	runningContainers := make([]string, 0, len(running))
	for _, c := range running {
		runningContainers = append(runningContainers, c.ID())
	}

	dfVolumes := make([]*entities.SystemDfVolumeReport, 0, len(vols))
	var reclaimableSize int64
	for _, v := range vols {
		var consInUse int
		mountPoint, err := v.MountPoint()
		if err != nil {
			return nil, err
		}
		if mountPoint == "" {
			// We can't get any info on this volume, as it's not
			// mounted.
			// TODO: fix this.
			continue
		}
		volSize, err := sizeOfPath(mountPoint)
		if err != nil {
			return nil, err
		}
		inUse, err := v.VolumeInUse()
		if err != nil {
			return nil, err
		}
		if len(inUse) == 0 {
			reclaimableSize += volSize
		}
		for _, viu := range inUse {
			if util.StringInSlice(viu, runningContainers) {
				consInUse++
			}
		}
		report := entities.SystemDfVolumeReport{
			VolumeName:      v.Name(),
			Links:           consInUse,
			Size:            volSize,
			ReclaimableSize: reclaimableSize,
		}
		dfVolumes = append(dfVolumes, &report)
	}
	return &entities.SystemDfReport{
		Images:     dfImages,
		Containers: dfContainers,
		Volumes:    dfVolumes,
	}, nil
}

// sizeOfPath determines the file usage of a given path. it was called volumeSize in v1
// and now is made to be generic and take a path instead of a libpod volume
func sizeOfPath(path string) (int64, error) {
	var size int64
	err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
		if err == nil && !info.IsDir() {
			size += info.Size()
		}
		return err
	})
	return size, err
}

func (se *SystemEngine) Reset(ctx context.Context) error {
	return se.Libpod.Reset(ctx)
}

func (se *SystemEngine) Renumber(ctx context.Context, flags *pflag.FlagSet, config *entities.PodmanConfig) error {
	return nil
}

func (se SystemEngine) Migrate(ctx context.Context, flags *pflag.FlagSet, config *entities.PodmanConfig, options entities.SystemMigrateOptions) error {
	return nil
}

func (se SystemEngine) Shutdown(ctx context.Context) {
	if err := se.Libpod.Shutdown(false); err != nil {
		logrus.Error(err)
	}
}

func unshareEnv(graphroot, runroot string) []string {
	return append(os.Environ(), "_CONTAINERS_USERNS_CONFIGURED=done",
		fmt.Sprintf("CONTAINERS_GRAPHROOT=%s", graphroot),
		fmt.Sprintf("CONTAINERS_RUNROOT=%s", runroot))
}

func (ic *ContainerEngine) Unshare(ctx context.Context, args []string, options entities.SystemUnshareOptions) error {
	unshare := func() error {
		cmd := exec.Command(args[0], args[1:]...)
		cmd.Env = unshareEnv(ic.Libpod.StorageConfig().GraphRoot, ic.Libpod.StorageConfig().RunRoot)
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		return cmd.Run()
	}

	if options.RootlessCNI {
		rootlesscni, err := ic.Libpod.GetRootlessCNINetNs(true)
		if err != nil {
			return err
		}
		// make sure to unlock, unshare can run for a long time
		rootlesscni.Lock.Unlock()
		defer rootlesscni.Cleanup(ic.Libpod)
		return rootlesscni.Do(unshare)
	}
	return unshare()
}

func (ic ContainerEngine) Version(ctx context.Context) (*entities.SystemVersionReport, error) {
	var report entities.SystemVersionReport
	v, err := define.GetVersion()
	if err != nil {
		return nil, err
	}
	report.Client = &v
	return &report, err
}
