//go:build linux
// +build linux

package overlay

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/Microsoft/hcsshim/internal/log"
	"github.com/Microsoft/hcsshim/internal/memory"
	"github.com/Microsoft/hcsshim/internal/oc"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"go.opencensus.io/trace"
	"golang.org/x/sys/unix"
)

// Test dependencies.
var (
	osMkdirAll  = os.MkdirAll
	osRemoveAll = os.RemoveAll
	unixMount   = unix.Mount
)

// processErrNoSpace logs disk space and inode information for `path` that we encountered the ENOSPC error on.
// This can be used to get a better view of whats going on on the disk at the time of the error.
func processErrNoSpace(ctx context.Context, path string, err error) {
	st := &unix.Statfs_t{}
	// Pass in filepath.Dir() of the path as if we got an error while creating the directory it definitely doesn't exist.
	// Take its parent, which should be on the same drive.
	if statErr := unix.Statfs(filepath.Dir(path), st); statErr != nil {
		log.G(ctx).WithError(statErr).WithField("path", filepath.Dir(path)).Warn("failed to get disk information for ENOSPC error")
		return
	}

	all := st.Blocks * uint64(st.Bsize)
	available := st.Bavail * uint64(st.Bsize)
	free := st.Bfree * uint64(st.Bsize)
	used := all - free

	toGigabyteStr := func(val uint64) string {
		return fmt.Sprintf("%.1f", float64(val)/float64(memory.GiB))
	}

	log.G(ctx).WithFields(logrus.Fields{
		"available-disk-space-GiB": toGigabyteStr(available),
		"free-disk-space-GiB":      toGigabyteStr(free),
		"used-disk-space-GiB":      toGigabyteStr(used),
		"total-inodes":             st.Files,
		"free-inodes":              st.Ffree,
		"path":                     path,
	}).WithError(err).Warn("got ENOSPC, gathering diagnostics")
}

// MountLayer first enforces the security policy for the container's layer paths
// and then calls Mount to mount the layer paths as an overlayfs.
func MountLayer(
	ctx context.Context,
	layerPaths []string,
	upperdirPath, workdirPath, rootfsPath string,
	readonly bool,
) (err error) {
	_, span := oc.StartSpan(ctx, "overlay::MountLayer")
	defer span.End()
	defer func() { oc.SetSpanStatus(span, err) }()

	return Mount(ctx, layerPaths, upperdirPath, workdirPath, rootfsPath, readonly)
}

// Mount creates an overlay mount with `basePaths` at `target`.
//
// If `upperdirPath != ""` the path will be created. On mount failure the
// created `upperdirPath` will be automatically cleaned up.
//
// If `workdirPath != ""` the path will be created. On mount failure the created
// `workdirPath` will be automatically cleaned up.
//
// Always creates `target`. On mount failure the created `target` will
// be automatically cleaned up.
func Mount(ctx context.Context, basePaths []string, upperdirPath, workdirPath, target string, readonly bool) (err error) {
	_, span := oc.StartSpan(ctx, "overlay::Mount")
	defer span.End()
	defer func() { oc.SetSpanStatus(span, err) }()

	lowerdir := strings.Join(basePaths, ":")
	span.AddAttributes(
		trace.StringAttribute("lowerdir", lowerdir),
		trace.StringAttribute("upperdirPath", upperdirPath),
		trace.StringAttribute("workdirPath", workdirPath),
		trace.StringAttribute("target", target),
		trace.BoolAttribute("readonly", readonly))

	// If we got an ENOSPC error on creating any directories, log disk space and inode info for
	//  the mount that the directory belongs to get a better view of the where the problem lies.
	defer func() {
		var perr *os.PathError
		if errors.As(err, &perr) && errors.Is(perr.Err, unix.ENOSPC) {
			processErrNoSpace(ctx, perr.Path, err)
		}
	}()

	if target == "" {
		return errors.New("cannot have empty target")
	}

	if readonly && (upperdirPath != "" || workdirPath != "") {
		return errors.Errorf("upperdirPath: %q, and workdirPath: %q must be empty when readonly==true", upperdirPath, workdirPath)
	}

	options := []string{"lowerdir=" + lowerdir}
	if upperdirPath != "" {
		if err := osMkdirAll(upperdirPath, 0755); err != nil {
			return errors.Wrap(err, "failed to create upper directory in scratch space")
		}
		defer func() {
			if err != nil {
				_ = osRemoveAll(upperdirPath)
			}
		}()
		options = append(options, "upperdir="+upperdirPath)
	}
	if workdirPath != "" {
		if err := osMkdirAll(workdirPath, 0755); err != nil {
			return errors.Wrap(err, "failed to create workdir in scratch space")
		}
		defer func() {
			if err != nil {
				_ = osRemoveAll(workdirPath)
			}
		}()
		options = append(options, "workdir="+workdirPath)
	}
	if err := osMkdirAll(target, 0755); err != nil {
		return errors.Wrapf(err, "failed to create directory for container root filesystem %s", target)
	}
	defer func() {
		if err != nil {
			_ = osRemoveAll(target)
		}
	}()
	var flags uintptr
	if readonly {
		flags |= unix.MS_RDONLY
	}
	if err := unixMount("overlay", target, "overlay", flags, strings.Join(options, ",")); err != nil {
		return errors.Wrapf(err, "failed to mount overlayfs at %s", target)
	}
	return nil
}
