// Copyright (c) 2017 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"bufio"
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"
	"syscall"

	vc "github.com/kata-containers/runtime/virtcontainers"
	"github.com/kata-containers/runtime/virtcontainers/pkg/oci"
	"github.com/opencontainers/runc/libcontainer/utils"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/sirupsen/logrus"
)

// Contants related to cgroup memory directory
const (
	cgroupsTasksFile = "tasks"
	cgroupsProcsFile = "cgroup.procs"
	cgroupsDirMode   = os.FileMode(0750)
	cgroupsFileMode  = os.FileMode(0640)

	// Filesystem type corresponding to CGROUP_SUPER_MAGIC as listed
	// here: http://man7.org/linux/man-pages/man2/statfs.2.html
	cgroupFsType = 0x27e0eb
)

var errNeedLinuxResource = errors.New("Linux resource cannot be empty")

var cgroupsDirPath string

var procMountInfo = "/proc/self/mountinfo"

// getContainerInfo returns the container status and its pod ID.
// It internally expands the container ID from the prefix provided.
func getContainerInfo(containerID string) (vc.ContainerStatus, string, error) {
	// container ID MUST be provided.
	if containerID == "" {
		return vc.ContainerStatus{}, "", fmt.Errorf("Missing container ID")
	}

	podStatusList, err := vci.ListPod()
	if err != nil {
		return vc.ContainerStatus{}, "", err
	}

	for _, podStatus := range podStatusList {
		for _, containerStatus := range podStatus.ContainersStatus {
			if containerStatus.ID == containerID {
				return containerStatus, podStatus.ID, nil
			}
		}
	}

	// Not finding a container should not trigger an error as
	// getContainerInfo is used for checking the existence and
	// the absence of a container ID.
	return vc.ContainerStatus{}, "", nil
}

func getExistingContainerInfo(containerID string) (vc.ContainerStatus, string, error) {
	cStatus, podID, err := getContainerInfo(containerID)
	if err != nil {
		return vc.ContainerStatus{}, "", err
	}

	// container ID MUST exist.
	if cStatus.ID == "" {
		return vc.ContainerStatus{}, "", fmt.Errorf("Container ID (%v) does not exist", containerID)
	}

	return cStatus, podID, nil
}

func validCreateParams(containerID, bundlePath string) (string, error) {
	// container ID MUST be provided.
	if containerID == "" {
		return "", fmt.Errorf("Missing container ID")
	}

	// container ID MUST be unique.
	cStatus, _, err := getContainerInfo(containerID)
	if err != nil {
		return "", err
	}

	if cStatus.ID != "" {
		return "", fmt.Errorf("ID already in use, unique ID should be provided")
	}

	// bundle path MUST be provided.
	if bundlePath == "" {
		return "", fmt.Errorf("Missing bundle path")
	}

	// bundle path MUST be valid.
	fileInfo, err := os.Stat(bundlePath)
	if err != nil {
		return "", fmt.Errorf("Invalid bundle path '%s': %s", bundlePath, err)
	}
	if fileInfo.IsDir() == false {
		return "", fmt.Errorf("Invalid bundle path '%s', it should be a directory", bundlePath)
	}

	resolved, err := resolvePath(bundlePath)
	if err != nil {
		return "", err
	}

	return resolved, nil
}

// processCgroupsPath process the cgroups path as expected from the
// OCI runtime specification. It returns a list of complete paths
// that should be created and used for every specified resource.
func processCgroupsPath(ociSpec oci.CompatOCISpec, isPod bool) ([]string, error) {
	var cgroupsPathList []string

	if ociSpec.Linux.CgroupsPath == "" {
		return []string{}, nil
	}

	if ociSpec.Linux.Resources == nil {
		return []string{}, nil
	}

	if ociSpec.Linux.Resources.Memory != nil {
		memCgroupsPath, err := processCgroupsPathForResource(ociSpec, "memory", isPod)
		if err != nil {
			return []string{}, err
		}

		if memCgroupsPath != "" {
			cgroupsPathList = append(cgroupsPathList, memCgroupsPath)
		}
	}

	if ociSpec.Linux.Resources.CPU != nil {
		cpuCgroupsPath, err := processCgroupsPathForResource(ociSpec, "cpu", isPod)
		if err != nil {
			return []string{}, err
		}

		if cpuCgroupsPath != "" {
			cgroupsPathList = append(cgroupsPathList, cpuCgroupsPath)
		}
	}

	if ociSpec.Linux.Resources.Pids != nil {
		pidsCgroupsPath, err := processCgroupsPathForResource(ociSpec, "pids", isPod)
		if err != nil {
			return []string{}, err
		}

		if pidsCgroupsPath != "" {
			cgroupsPathList = append(cgroupsPathList, pidsCgroupsPath)
		}
	}

	if ociSpec.Linux.Resources.BlockIO != nil {
		blkIOCgroupsPath, err := processCgroupsPathForResource(ociSpec, "blkio", isPod)
		if err != nil {
			return []string{}, err
		}

		if blkIOCgroupsPath != "" {
			cgroupsPathList = append(cgroupsPathList, blkIOCgroupsPath)
		}
	}

	return cgroupsPathList, nil
}

func processCgroupsPathForResource(ociSpec oci.CompatOCISpec, resource string, isPod bool) (string, error) {
	if resource == "" {
		return "", errNeedLinuxResource
	}

	var err error
	cgroupsDirPath, err = getCgroupsDirPath(procMountInfo)
	if err != nil {
		return "", fmt.Errorf("get CgroupsDirPath error: %s", err)
	}

	// Relative cgroups path provided.
	if filepath.IsAbs(ociSpec.Linux.CgroupsPath) == false {
		return filepath.Join(cgroupsDirPath, resource, ociSpec.Linux.CgroupsPath), nil
	}

	// Absolute cgroups path provided.
	var cgroupMount specs.Mount
	cgroupMountFound := false
	for _, mount := range ociSpec.Mounts {
		if mount.Type == "cgroup" {
			cgroupMount = mount
			cgroupMountFound = true
			break
		}
	}

	if !cgroupMountFound {
		// According to the OCI spec, an absolute path should be
		// interpreted as relative to the system cgroup mount point
		// when there is no cgroup mount point.
		return filepath.Join(cgroupsDirPath, resource, ociSpec.Linux.CgroupsPath), nil
	}

	if cgroupMount.Destination == "" {
		return "", fmt.Errorf("cgroupsPath is absolute, cgroup mount destination cannot be empty")
	}

	cgroupPath := filepath.Join(cgroupMount.Destination, resource)

	// It is not an error to have this cgroup not mounted. It is usually
	// due to an old kernel version with missing support for specific
	// cgroups.
	fields := logrus.Fields{
		"path": cgroupPath,
		"type": "cgroup",
	}

	if !isCgroupMounted(cgroupPath) {
		ccLog.WithFields(fields).Info("path not mounted")
		return "", nil
	}

	ccLog.WithFields(fields).Info("path mounted")

	return filepath.Join(cgroupPath, ociSpec.Linux.CgroupsPath), nil
}

func isCgroupMounted(cgroupPath string) bool {
	var statFs syscall.Statfs_t

	if err := syscall.Statfs(cgroupPath, &statFs); err != nil {
		return false
	}

	if statFs.Type != int64(cgroupFsType) {
		return false
	}

	return true
}

func setupConsole(consolePath, consoleSockPath string) (string, error) {
	if consolePath != "" {
		return consolePath, nil
	}

	if consoleSockPath == "" {
		return "", nil
	}

	console, err := newConsole()
	if err != nil {
		return "", err
	}
	defer console.master.Close()

	// Open the socket path provided by the caller
	conn, err := net.Dial("unix", consoleSockPath)
	if err != nil {
		return "", err
	}

	uConn, ok := conn.(*net.UnixConn)
	if !ok {
		return "", fmt.Errorf("casting to *net.UnixConn failed")
	}

	socket, err := uConn.File()
	if err != nil {
		return "", err
	}

	// Send the parent fd through the provided socket
	if err := utils.SendFd(socket, console.master); err != nil {
		return "", err
	}

	return console.slavePath, nil
}

func noNeedForOutput(detach bool, tty bool) bool {
	if !detach {
		return false
	}

	if !tty {
		return false
	}

	return true
}

func getCgroupsDirPath(mountInfoFile string) (string, error) {
	if cgroupsDirPath != "" {
		return cgroupsDirPath, nil
	}

	f, err := os.Open(mountInfoFile)
	if err != nil {
		return "", err
	}
	defer f.Close()

	var cgroupRootPath string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		text := scanner.Text()
		index := strings.Index(text, " - ")
		if index < 0 {
			continue
		}
		fields := strings.Split(text, " ")
		postSeparatorFields := strings.Fields(text[index+3:])
		numPostFields := len(postSeparatorFields)

		if len(fields) < 5 || postSeparatorFields[0] != "cgroup" || numPostFields < 3 {
			continue
		}

		cgroupRootPath = filepath.Dir(fields[4])
		break
	}

	if _, err = os.Stat(cgroupRootPath); err != nil {
		return "", err
	}

	return cgroupRootPath, nil
}
