package main

import (
	"encoding/json"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"golang.org/x/sys/unix"
	"net"

	"github.com/coreos/go-systemd/v22/daemon"
	"github.com/godbus/dbus/v5"
	"github.com/landlock-lsm/go-landlock/landlock"
	landlockSyscall "github.com/landlock-lsm/go-landlock/landlock/syscall"
)

type PassFiles struct {
	// FileMap is a map that contains [host path string](docid string)
	FileMap		map[string]string
}

type ResponseField struct {
	Success			bool
	ID			int
}

type StartNofifyMsg struct {
	cmd			*exec.Cmd
	UDS			[]net.Listener
	sockDir			string

}

var (
	startNotifier		= make(chan StartNofifyMsg, 2)
	terminateNotify		= make(chan int, 1)
	procAttr		= &syscall.SysProcAttr{
		Pdeathsig:	syscall.SIGKILL,
	}
)

func engageLandlock () {
	config, err := landlock.NewConfig(landlock.ScopedSet(landlockSyscall.ScopeSignal))
	if err != nil {
		warn.Println("Could not restrict sending signals: " + err.Error())
	} else {
		config.RestrictScoped()
	}

	mountInfoCfg := os.Getenv("_portableEnableLandlock")
	if len(mountInfoCfg) == 0 {
		return
	} else {
		fullAccRule := landlock.AccessFSSet(landlockSyscall.AccessFSExecute|landlockSyscall.AccessFSWriteFile|landlockSyscall.AccessFSReadFile|
landlockSyscall.AccessFSReadDir|landlockSyscall.AccessFSRemoveDir|landlockSyscall.AccessFSRemoveFile|landlockSyscall.AccessFSMakeChar|landlockSyscall.AccessFSMakeDir|landlockSyscall.AccessFSMakeReg|landlockSyscall.AccessFSMakeSock|landlockSyscall.AccessFSMakeFifo|landlockSyscall.AccessFSMakeBlock|landlockSyscall.AccessFSMakeSym|landlockSyscall.AccessFSRefer|landlockSyscall.AccessFSTruncate|landlockSyscall.AccessFSIoctlDev)
		dirAccRule := landlock.AccessFSSet(landlockSyscall.AccessFSExecute|landlockSyscall.AccessFSWriteFile|landlockSyscall.AccessFSReadFile|
landlockSyscall.AccessFSReadDir|landlockSyscall.AccessFSRemoveDir|landlockSyscall.AccessFSRemoveFile|landlockSyscall.AccessFSMakeDir|landlockSyscall.AccessFSMakeReg|landlockSyscall.AccessFSMakeSock|landlockSyscall.AccessFSMakeFifo|landlockSyscall.AccessFSMakeSym|landlockSyscall.AccessFSRefer|landlockSyscall.AccessFSTruncate)
		dirRoRule := landlock.AccessFSSet(landlockSyscall.AccessFSExecute|landlockSyscall.AccessFSReadFile|
landlockSyscall.AccessFSReadDir)
		homeDir, err := os.UserHomeDir()
		if err != nil {
			warn.Println("Could not get user home: " + err.Error())
			return
		}
		err = landlock.V6.RestrictPaths(
			landlock.PathAccess(landlockSyscall.AccessFSReadDir, "/"), // Root
			landlock.PathAccess(dirRoRule, "/bin"),
			landlock.PathAccess(fullAccRule, "/dev"),
			landlock.PathAccess(fullAccRule, "/proc"),
			landlock.PathAccess(fullAccRule, "/sys"),
			landlock.PathAccess(dirRoRule, "/etc"),
			landlock.PathAccess(dirRoRule, "/lib"),
			landlock.PathAccess(dirRoRule, "/lib64"),
			landlock.PathAccess(dirRoRule, "/opt"),
			landlock.PathAccess(dirRoRule, "/sbin"),
			landlock.PathAccess(dirRoRule, "/usr"),
			landlock.ROFiles("/.flatpak-info"),
			landlock.PathAccess(dirAccRule, "/run"),
			landlock.PathAccess(dirAccRule, "/tmp"),
			landlock.PathAccess(dirAccRule, homeDir),
		)
		if err != nil {
			warn.Println("Could not enforce file system landlock: " + err.Error())
		}
	}
}

func updateSd(count int) {
	status := "STATUS=" + "Tracking processes: " + strconv.Itoa(count)
	sent, err := daemon.SdNotify(false, status)
	if sent == true {
		debug.Println("Updated tracking status: ", count)
	} else {
		if err == nil {
			warn.Println("Notification of daemon status not supported")
		} else {
			warn.Println("Failed to update status: " + err.Error())
		}
	}
}

type StartRequest struct {
	Exec		[]string
	CustomTarget	bool
	Files		PassFiles
}

func startMaster(targetExec []string, targetArgs []string) {
	var startReq StartNofifyMsg
	rawEnv := os.Getenv("_portableHelperExtraFiles")
	var args []string
	if len(rawEnv) > 0 {
		targetExec = cmdlineReplacer(targetExec)
		targetArgs = cmdlineReplacer(targetArgs)
	}
	args = append(targetExec, targetArgs...)

	startCmd := exec.Command(args[0], args[1:]...)
	debug.Println("Starting main application", args[0], "with cmdline:", args[1:])
	go daemon.SdNotify(false, daemon.SdNotifyReady)
	startReq.cmd = startCmd
	startNotifier <- startReq
}

func sendPidFd() {
	pid := os.Getpid()
	var st unix.Stat_t

	pidfd, err := unix.PidfdOpen(pid, 0)
	if err != nil {
		warn.Println("Could not obtain PIDFD: " + err.Error())
		return
	}
	err = unix.Fstat(pidfd, &st)
	if err != nil {
		warn.Println("Could not obtain PIDFD inode: " + err.Error())
		return
	}
	res, err := daemon.SdNotify(false, "MAINPIDFDID=" + strconv.Itoa(int(st.Ino)))
	if err != nil {
		warn.Println("Could not set Main PID: " + err.Error())
	} else if res == false {
		warn.Println("Could not set Main PID: " + "unknown error")
	}
}

func terminateWatcher(blocker chan int, conn *dbus.Conn) {
	busName := "top.kimiblock.portable." + os.Getenv("appID")
	busObj := conn.Object(busName, "/top/kimiblock/portable/daemon")
	<- blocker
	if packageHasInhibit {
		name := conn.Names()[0]
		name = strings.ReplaceAll(name, ".", "_")
		name = strings.TrimPrefix(name, ":")
		path := "/org/freedesktop/portal/desktop/request/"
		path = path + name + "/" + inhibitRequest
		reqObj := conn.Object(
			"org.freedesktop.portal.Desktop",
			dbus.ObjectPath(path),
		)
		call := reqObj.Call(
			"org.freedesktop.portal.Request.Close",
			dbus.FlagNoReplyExpected,
		)
		if call.Err != nil {
			warn.Println("Could not unregister inhibit:", call.Err)
		}
	}
	warn.Println("Requesting termination...")
	call := busObj.Call("top.kimiblock.Portable.Controller.Stop", dbus.FlagNoReplyExpected)
	if call.Err != nil {
		panic(call.Err)
	}
	os.Exit(0)
}

func main () {
	var wg sync.WaitGroup
	wg.Go(func() {
		engageLandlock()
	})
	wg.Go(func() {
		rawEnv := os.Getenv("_portableHelperExtraFiles")
		if len(rawEnv) > 0 {
			var decoded PassFiles
			err := json.Unmarshal([]byte(rawEnv), &decoded)
			if err != nil {
				warn.Println("Could not decode JSON from environment variable: " + err.Error())
				return
			}
			filemapAddMap(decoded.FileMap)
		}
	})
	var bus *dbus.Conn
	wg.Go(func() {
		var err error
		bus, err = dbus.ConnectSessionBus()
		if err != nil {
			panic("Could not connect to session bus: " + err.Error())
		}
		env := os.Getenv("_portableInhibit")
		if env == "1" {
			packageHasInhibit = true
		} else {
			return
		}
		callInhibit(bus)
	})
	go startCounterV2(startNotifier)
	go sendPidFd()

	// This is horrible, but launchTarget may have spaces
	var rawTarget = os.Getenv("_portableLaunchTarget")
	var targetSlice = []string{
		rawTarget,
	}
	targetArgs := os.Args[1:]
	debug.Println("Got raw command line arguments:", targetArgs)
	exposedEnvs := os.Getenv("_portableHelperExtraFiles")
	if len(exposedEnvs) > 0 {
		var exposeList PassFiles
		json.Unmarshal([]byte(exposedEnvs), &exposeList)
	}
	wg.Wait()
	go busAuxStart(bus, targetSlice)
	if os.Getenv("_portableDebug") == "1" {
		targetSlice = []string{
			"/usr/bin/bash",
			"--noprofile",
			"--rcfile", "/run/bashrc",
			"-i",
		}
		targetArgs = []string{}
	} else if os.Getenv("_portableBusActivate") == "1" {
		busArgs := []string{}
		err := json.Unmarshal([]byte(os.Getenv("_portableBusActivateArgs")), &busArgs)
		if err != nil {
			panic(err)
		}
		if len(os.Args) > 1 {
			targetArgs = os.Args[1:]
		} else {
			targetArgs = []string{}
		}
		targetSlice = busArgs
	}
	go startMaster(targetSlice, targetArgs)
	go terminateWatcher(terminateNotify, bus)
	select {}
}