// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

//go:build linux
// +build linux

package nsenter

import (
	"fmt"
	"os"
	"os/exec"
	"strconv"

	"golang.org/x/sys/unix"
	"golang.org/x/xerrors"
)

type Namespace int

const (
	// NamespaceMount refers to the mount namespace
	NamespaceMount = iota
	// NamespaceNet refers to the network namespace
	NamespaceNet
	// NamespaceNet refers to the network namespace
	NamespacePID
)

// Run executes a workspacekit handler in a namespace.
// Use preflight to check libseccomp.NotifIDValid().
func Run(pid int, args []string, addFD []*os.File, enterNamespace ...Namespace) error {
	nss := []struct {
		Env    string
		Source string
		Flags  int
		NS     Namespace
	}{
		{"_LIBNSENTER_ROOTFD", fmt.Sprintf("/proc/%d/root", pid), unix.O_PATH, -1},
		{"_LIBNSENTER_CWDFD", fmt.Sprintf("/proc/%d/cwd", pid), unix.O_PATH, -1},
		{"_LIBNSENTER_MNTNSFD", fmt.Sprintf("/proc/%d/ns/mnt", pid), os.O_RDONLY, NamespaceMount},
		{"_LIBNSENTER_NETNSFD", fmt.Sprintf("/proc/%d/ns/net", pid), os.O_RDONLY, NamespaceNet},
		{"_LIBNSENTER_PIDNSFD", fmt.Sprintf("/proc/%d/ns/pid", pid), os.O_RDONLY, NamespacePID},
	}

	stdioFdCount := 3
	cmd := exec.Command("/proc/self/exe", append([]string{"handler"}, args...)...)
	cmd.ExtraFiles = append(cmd.ExtraFiles, addFD...)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "_LIBNSENTER_INIT=1")
	for _, ns := range nss {
		var enter bool
		if ns.NS == -1 {
			enter = true
		} else {
			for _, s := range enterNamespace {
				if ns.NS == s {
					enter = true
					break
				}
			}
		}
		if !enter {
			continue
		}

		f, err := os.OpenFile(ns.Source, ns.Flags, 0)
		if err != nil {
			return xerrors.Errorf("cannot open %s: %w", ns.Source, err)
		}
		defer f.Close()
		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", ns.Env, stdioFdCount+len(cmd.ExtraFiles)))
		cmd.ExtraFiles = append(cmd.ExtraFiles, f)
	}

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Stdin = os.Stdin
	err := cmd.Run()
	if err != nil {
		return xerrors.Errorf("cannot run handler: %w", err)
	}
	return nil
}

// Mount executes mount in the mount namespace of PID
func Mount(pid int, source, target string, fstype string, flags int, data string) error {
	args := []string{"mount",
		"--source", source,
		"--target", target,
		"--flags", strconv.Itoa(flags),
	}
	if fstype != "" {
		args = append(args, "--fstype", fstype)
	}
	if data != "" {
		args = append(args, "--data", data)
	}
	return Run(pid, args, nil, NamespaceMount)
}

func MoveMount(pid int, fromFD *os.File, target string) error {
	args := []string{"move-mount",
		"--fd", "3",
		"--dest", target,
	}
	return Run(pid, args, []*os.File{fromFD}, NamespaceMount)
}
