package main

import (
	"encoding/json"
	"log"
	"os"
	"path"

	"git.gensokyo.uk/security/fortify/dbus"
	"git.gensokyo.uk/security/fortify/fst"
	"git.gensokyo.uk/security/fortify/sandbox/seccomp"
	"git.gensokyo.uk/security/fortify/system"
)

type appInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`

	// passed through to [fst.Config]
	ID string `json:"id"`
	// passed through to [fst.Config]
	AppID int `json:"app_id"`
	// passed through to [fst.Config]
	Groups []string `json:"groups,omitempty"`
	// passed through to [fst.Config]
	Devel bool `json:"devel,omitempty"`
	// passed through to [fst.Config]
	Userns bool `json:"userns,omitempty"`
	// passed through to [fst.Config]
	Net bool `json:"net,omitempty"`
	// passed through to [fst.Config]
	Dev bool `json:"dev,omitempty"`
	// passed through to [fst.Config]
	Tty bool `json:"tty,omitempty"`
	// passed through to [fst.Config]
	MapRealUID bool `json:"map_real_uid,omitempty"`
	// passed through to [fst.Config]
	DirectWayland bool `json:"direct_wayland,omitempty"`
	// passed through to [fst.Config]
	SystemBus *dbus.Config `json:"system_bus,omitempty"`
	// passed through to [fst.Config]
	SessionBus *dbus.Config `json:"session_bus,omitempty"`
	// passed through to [fst.Config]
	Enablements system.Enablement `json:"enablements"`

	// passed through to [fst.Config]
	Multiarch bool `json:"multiarch,omitempty"`
	// passed through to [fst.Config]
	Bluetooth bool `json:"bluetooth,omitempty"`

	// allow gpu access within sandbox
	GPU bool `json:"gpu"`
	// store path to nixGL mesa wrappers
	Mesa string `json:"mesa,omitempty"`
	// store path to nixGL source
	NixGL string `json:"nix_gl,omitempty"`
	// store path to activate-and-exec script
	Launcher string `json:"launcher"`
	// store path to /run/current-system
	CurrentSystem string `json:"current_system"`
	// store path to home-manager activation package
	ActivationPackage string `json:"activation_package"`
}

func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
	config := &fst.Config{
		ID:   app.ID,
		Path: argv[0],
		Args: argv,
		Confinement: fst.ConfinementConfig{
			AppID:    app.AppID,
			Groups:   app.Groups,
			Username: "fortify",
			Inner:    path.Join("/data/data", app.ID),
			Outer:    pathSet.homeDir,
			Shell:    shellPath,
			Sandbox: &fst.SandboxConfig{
				Hostname:      formatHostname(app.Name),
				Devel:         app.Devel,
				Userns:        app.Userns,
				Net:           app.Net,
				Dev:           app.Dev,
				Tty:           app.Tty || flagDropShell,
				MapRealUID:    app.MapRealUID,
				DirectWayland: app.DirectWayland,
				Filesystem: []*fst.FilesystemConfig{
					{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
					{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
					{Src: "/etc/resolv.conf"},
					{Src: "/sys/block"},
					{Src: "/sys/bus"},
					{Src: "/sys/class"},
					{Src: "/sys/dev"},
					{Src: "/sys/devices"},
				},
				Link: [][2]string{
					{app.CurrentSystem, "/run/current-system"},
					{"/run/current-system/sw/bin", "/bin"},
					{"/run/current-system/sw/bin", "/usr/bin"},
				},
				Etc:     path.Join(pathSet.cacheDir, "etc"),
				AutoEtc: true,
			},
			ExtraPerms: []*fst.ExtraPermConfig{
				{Path: dataHome, Execute: true},
				{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
			},
			SystemBus:   app.SystemBus,
			SessionBus:  app.SessionBus,
			Enablements: app.Enablements,
		},
	}
	if app.Multiarch {
		config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
	}
	if app.Bluetooth {
		config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
	}
	return config
}

func loadAppInfo(name string, beforeFail func()) *appInfo {
	bundle := new(appInfo)
	if f, err := os.Open(name); err != nil {
		beforeFail()
		log.Fatalf("cannot open bundle: %v", err)
	} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
		beforeFail()
		log.Fatalf("cannot parse bundle metadata: %v", err)
	} else if err = f.Close(); err != nil {
		log.Printf("cannot close bundle metadata: %v", err)
		// not fatal
	}

	if bundle.ID == "" {
		beforeFail()
		log.Fatal("application identifier must not be empty")
	}

	return bundle
}

func formatHostname(name string) string {
	if h, err := os.Hostname(); err != nil {
		log.Printf("cannot get hostname: %v", err)
		return "fortify-" + name
	} else {
		return h + "-" + name
	}
}