// Copyright (c) 2020 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.

package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"github.com/prometheus/procfs"
	"golang.org/x/sys/unix"
	"golang.org/x/xerrors"

	"github.com/gitpod-io/gitpod/test/pkg/agent/workspace/api"
	"github.com/gitpod-io/gitpod/test/pkg/integration"
)

func main() {
	done := make(chan struct{})
	go func() {
		mux := http.NewServeMux()
		mux.Handle("/shutdown", shugtdownHandler(done))
		_ = http.ListenAndServe(":8080", mux)
	}()

	err := enterSupervisorNamespaces()
	if err != nil {
		panic(fmt.Sprintf("enterSupervisorNamespaces: %v", err))
	}

	integration.ServeAgent(done, new(WorkspaceAgent))
}

func shugtdownHandler(done chan struct{}) http.HandlerFunc {
	return func(w http.ResponseWriter, _ *http.Request) {
		close(done)
		w.Write([]byte("shutdown"))
		w.WriteHeader(http.StatusOK)
	}
}

func enterSupervisorNamespaces() error {
	if os.Getenv("AGENT_IN_RING2") != "" {
		return nil
	}

	nsenter, err := exec.LookPath("nsenter")
	if err != nil {
		return xerrors.Errorf("cannot find nsenter")
	}

	// This agent expectes to be called using the workspacekit lift (i.e. in ring1).
	// We then enter the PID and mount namespace of supervisor.
	// First, we need to find the supervisor process
	proc, err := procfs.NewFS("/proc")
	if err != nil {
		return err
	}
	procs, err := proc.AllProcs()
	if err != nil {
		return err
	}
	var supervisorPID int
	for _, p := range procs {
		cmd, _ := p.CmdLine()
		for _, c := range cmd {
			if strings.HasSuffix(c, "supervisor") {
				supervisorPID = p.PID
				break
			}
		}
		if supervisorPID != 0 {
			break
		}
	}
	if supervisorPID == 0 {
		return xerrors.Errorf("no supervisor process found")
	}

	// Then we copy ourselves to a location that we can access from the supervisor NS
	self, err := os.Executable()
	if err != nil {
		return err
	}
	fn := fmt.Sprintf("/proc/%d/root/agent", supervisorPID)
	dst, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY, 0755)
	if err != nil {
		return err
	}
	selfFD, err := os.Open(self)
	if err != nil {
		return err
	}
	defer selfFD.Close()
	_, err = io.Copy(dst, selfFD)
	if err != nil {
		return xerrors.Errorf("error copying agent: %w", err)
	}

	return unix.Exec(nsenter, append([]string{nsenter, "-t", strconv.Itoa(supervisorPID), "-m", "-p", "/agent"}, os.Args[1:]...), append(os.Environ(), "AGENT_IN_RING2=true"))
}

// WorkspaceAgent provides ingteration test services from within a workspace
type WorkspaceAgent struct {
}

// ListDir lists a directory's content
func (*WorkspaceAgent) ListDir(req *api.ListDirRequest, resp *api.ListDirResponse) error {
	dc, err := os.ReadDir(req.Dir)
	if err != nil {
		return err
	}

	*resp = api.ListDirResponse{}
	for _, c := range dc {
		resp.Files = append(resp.Files, c.Name())
	}
	return nil
}

// WriteFile writes a file in the workspace
func (*WorkspaceAgent) WriteFile(req *api.WriteFileRequest, resp *api.WriteFileResponse) (err error) {
	err = os.WriteFile(req.Path, req.Content, req.Mode)
	if err != nil {
		return
	}

	*resp = api.WriteFileResponse{}
	return
}

// Exec executes a command in the workspace
func (*WorkspaceAgent) Exec(req *api.ExecRequest, resp *api.ExecResponse) (err error) {
	cmd := exec.Command(req.Command, req.Args...)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, req.Env...)
	if req.Dir != "" {
		cmd.Dir = req.Dir
	}
	var (
		stdout bytes.Buffer
		stderr bytes.Buffer
	)
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	err = cmd.Run()

	var rc int
	if err != nil {
		exitError, ok := err.(*exec.ExitError)
		if !ok {
			fullCommand := strings.Join(append([]string{req.Command}, req.Args...), " ")
			return xerrors.Errorf("%s: %w", fullCommand, err)
		}
		rc = exitError.ExitCode()
		err = nil
	}

	*resp = api.ExecResponse{
		ExitCode: rc,
		Stdout:   stdout.String(),
		Stderr:   stderr.String(),
	}
	return
}

func (*WorkspaceAgent) BurnCpu(req *api.BurnCpuRequest, resp *api.BurnCpuResponse) error {
	done := make(chan int)
	for i := 0; i < int(req.Procs); i++ {
		go func() {
			for {
				select {
				case <-done:
					return
				default:
				}
			}
		}()
	}

	time.Sleep(req.Timeout)
	close(done)
	return nil
}
