package libenclave // import "github.com/opencontainers/runc/libenclave"

import (
	"fmt"
	"github.com/opencontainers/runc/libcontainer/utils"
	pb "github.com/opencontainers/runc/libenclave/proto"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"
	"net"
	"os"
	"path/filepath"
	"strings"
)

const agentServicePath = "agent.sock"

var instanceId int

func CreateParentAgentPipe(root string, uid, gid int) (lnFile *os.File, err error) {
	// The Linux kernel only allows unix domain socket paths less than
	// 108 bytes, so chdir() to avoid errors caused by excessively long
	// path.
	cwd, err := os.Getwd()
	if err != nil {
		return nil, err
	}
	if err = os.Chdir(root); err != nil {
		return nil, err
	}
	defer os.Chdir(cwd)

	ln, err := net.Listen("unix", agentServicePath)
	if err != nil {
		return nil, err
	}
	defer func() {
		if err != nil {
			ln.Close()
		}
	}()
	unixListener, ok := ln.(*net.UnixListener)
	if !ok {
		return nil, fmt.Errorf("casting to UnixListener failed")
	}
	// By default the underlying socket file is removed when the
	// listener is closed, no matter what the opened file count is.
	// This behavior will cause the connection failure when the
	// incoming child runelet attempts to connect to agent service.
	unixListener.SetUnlinkOnClose(false)
	defer func() {
		if err != nil {
			unixListener.SetUnlinkOnClose(true)
		}
	}()
	lnFile, err = unixListener.File()
	if err != nil {
		return nil, err
	}
	defer func() {
		if err != nil {
			lnFile.Close()
		}
	}()

	err = unix.Chmod(agentServicePath, 0622)
	if err != nil {
		return nil, err
	}
	err = os.Chown(agentServicePath, uid, gid)
	if err != nil {
		return nil, err
	}

	return lnFile, nil
}

func CreateChildAgentPipe(root string) (*os.File, error) {
	path := filepath.Join(root, "agent.sock")
	conn, err := net.Dial("unix", path)
	if err != nil {
		return nil, err
	}
	defer conn.Close()

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

	connFile, err := unixConn.File()
	if err != nil {
		return nil, err
	}
	return connFile, nil
}

func startAgentService(agentPipe *os.File, notifyExit <-chan struct{}) <-chan struct{} {
	isDead := make(chan struct{})
	go func() {
		defer close(isDead)

		ln, err := net.FileListener(agentPipe)
		if err != nil {
			return
		}
		defer ln.Close()
		uln, ok := ln.(*net.UnixListener)
		if !ok {
			return
		}

		for {
			select {
			case <-notifyExit:
				logrus.Debug("TODO: notifying exit event")
				return
			default:
			}

			client, err := uln.Accept()
			if err != nil {
				logrus.Debugf("accept() failed with %v", err)
				return
			}
			instanceId += 1
			go handleRequest(client, instanceId)
		}
	}()

	return isDead
}

func handleRequest(conn net.Conn, id int) {
	defer conn.Close()

	if enclaveRuntime == nil {
		logrus.Fatal("race with parent runelet")
		return
	}

	var err error
	resp := &pb.AgentServiceResponse{}
	exitCode := int32(1)

	req := &pb.AgentServiceRequest{}
	if err = protoBufRead(conn, req); err != nil {
		return
	}

	c, ok := conn.(*net.UnixConn)
	if !ok {
		err = fmt.Errorf("casting to UnixConn failed")
		return
	}
	connFile, err := c.File()
	if err != nil {
		return
	}
	defer connFile.Close()

	if req.Attest != nil {
		logrus.Infof("In function handleRequest: get an attest request")
		resp.Attest = &pb.AgentServiceResponse_Attest{}
		iasReport, err := enclaveRuntime.LaunchAttestation(req.Attest.Spid,
			req.Attest.SubscriptionKey,
			req.Attest.Product,
			req.Attest.QuoteType)
		if err != nil {
			resp.Attest.Error = fmt.Sprint(err)
		} else {
			exitCode = 0
		}

		resp.Attest.ExitCode = exitCode
		resp.Attest.StatusCode = iasReport["StatusCode"]
		resp.Attest.RequestID = iasReport["Request-ID"]
		resp.Attest.XIasreportSignature = iasReport["X-Iasreport-Signature"]
		resp.Attest.XIasreportSigningCertificate = iasReport["X-Iasreport-Signing-Certificate"]
		resp.Attest.ContentLength = iasReport["ContentLength"]
		resp.Attest.ContentType = iasReport["Content-Type"]
		resp.Attest.Body = iasReport["Body"]

		protoBufWrite(conn, resp)
		return
	}

	resp.Exec = &pb.AgentServiceResponse_Execute{}
	defer func() {
		resp.Exec.ExitCode = exitCode
		if err != nil {
			resp.Exec.Error = fmt.Sprint(err)
		}
		protoBufWrite(conn, resp)
	}()

	// Retrieve signal pipe.
	signalPipe, err := utils.RecvFd(connFile)
	if err != nil {
		return
	}
	go relaySignal(signalPipe, id)

	// Retrieve stdio fds.
	stdin, err := utils.RecvFd(connFile)
	if err != nil {
		return
	}
	defer stdin.Close()
	stdout, err := utils.RecvFd(connFile)
	if err != nil {
		return
	}
	defer stdout.Close()
	stderr, err := utils.RecvFd(connFile)
	if err != nil {
		return
	}
	defer stderr.Close()
	stdio := [3]*os.File{
		stdin, stdout, stderr,
	}

	cmd := req.Exec.GetArgv()
	envp := req.Exec.GetEnvp()
	exitCode, err = enclaveRuntime.ExecutePayload(strings.Split(cmd, " "), strings.Split(envp, " "), stdio)
	if err != nil {
		return
	}

	// TODO: sync up with relaySignal()

	logrus.Debug("remote exec normally exits")
}

func relaySignal(signalPipe *os.File, id int) {
	defer signalPipe.Close()

	for {
		req := &pb.AgentServiceRequest{}
		if err := protoBufRead(signalPipe, req); err != nil {
			return
		}

		err := enclaveRuntime.KillPayload(id, int(req.Kill.Sig))
		if err != nil {
			logrus.Errorf("unable to kill payload with sig %d by %d: %v\n", int(req.Kill.Sig), id, err)
			return
		}
	}
}
