package app

import (
	"errors"
	"fmt"
	"io/fs"
	"path"
	"regexp"
	"strconv"

	"git.gensokyo.uk/security/fortify/acl"
	"git.gensokyo.uk/security/fortify/dbus"
	"git.gensokyo.uk/security/fortify/fst"
	"git.gensokyo.uk/security/fortify/internal/fmsg"
	"git.gensokyo.uk/security/fortify/internal/linux"
	"git.gensokyo.uk/security/fortify/internal/state"
	"git.gensokyo.uk/security/fortify/internal/system"
)

var (
	ErrConfig = errors.New("no configuration to seal")
	ErrUser   = errors.New("invalid aid")
	ErrHome   = errors.New("invalid home directory")
	ErrName   = errors.New("invalid username")
)

var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")

// appSeal seals the application with child-related information
type appSeal struct {
	// app unique ID string representation
	id string
	// dbus proxy message buffer retriever
	dbusMsg func(f func(msgbuf []string))

	// freedesktop application ID
	fid string
	// argv to start process with in the final confined environment
	command []string
	// persistent process state store
	store state.Store

	// process-specific share directory path
	share string
	// process-specific share directory path local to XDG_RUNTIME_DIR
	shareLocal string

	// pass-through enablement tracking from config
	et system.Enablements
	// wayland socket direct access
	directWayland bool
	// extra UpdatePerm ops
	extraPerms []*sealedExtraPerm

	// prevents sharing from happening twice
	shared bool
	// seal system-level component
	sys *appSealSys

	linux.Paths

	// protected by upstream mutex
}

type sealedExtraPerm struct {
	name   string
	perms  acl.Perms
	ensure bool
}

// Seal seals the app launch context
func (a *app) Seal(config *fst.Config) error {
	a.lock.Lock()
	defer a.lock.Unlock()

	if a.seal != nil {
		panic("app sealed twice")
	}

	if config == nil {
		return fmsg.WrapError(ErrConfig,
			"attempted to seal app with nil config")
	}

	// create seal
	seal := new(appSeal)

	// fetch system constants
	seal.Paths = a.os.Paths()

	// pass through config values
	seal.id = a.id.String()
	seal.fid = config.ID
	seal.command = config.Command

	// create seal system component
	seal.sys = new(appSealSys)

	// mapped uid
	if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.MapRealUID {
		seal.sys.mappedID = a.os.Geteuid()
	} else {
		seal.sys.mappedID = 65534
	}
	seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID)
	seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString)

	// validate uid and set user info
	if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
		return fmsg.WrapError(ErrUser,
			fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
	}
	seal.sys.user = appUser{
		aid:      config.Confinement.AppID,
		as:       strconv.Itoa(config.Confinement.AppID),
		data:     config.Confinement.Outer,
		home:     config.Confinement.Inner,
		username: config.Confinement.Username,
	}
	if seal.sys.user.username == "" {
		seal.sys.user.username = "chronos"
	} else if !posixUsername.MatchString(seal.sys.user.username) {
		return fmsg.WrapError(ErrName,
			fmt.Sprintf("invalid user name %q", seal.sys.user.username))
	}
	if seal.sys.user.data == "" || !path.IsAbs(seal.sys.user.data) {
		return fmsg.WrapError(ErrHome,
			fmt.Sprintf("invalid home directory %q", seal.sys.user.data))
	}
	if seal.sys.user.home == "" {
		seal.sys.user.home = seal.sys.user.data
	}

	// invoke fsu for full uid
	if u, err := a.os.Uid(seal.sys.user.aid); err != nil {
		return fmsg.WrapErrorSuffix(err,
			"cannot obtain uid from fsu:")
	} else {
		seal.sys.user.uid = u
		seal.sys.user.us = strconv.Itoa(u)
	}

	// resolve supplementary group ids from names
	seal.sys.user.supp = make([]string, len(config.Confinement.Groups))
	for i, name := range config.Confinement.Groups {
		if g, err := a.os.LookupGroup(name); err != nil {
			return fmsg.WrapError(err,
				fmt.Sprintf("unknown group %q", name))
		} else {
			seal.sys.user.supp[i] = g.Gid
		}
	}

	// build extra perms
	seal.extraPerms = make([]*sealedExtraPerm, len(config.Confinement.ExtraPerms))
	for i, p := range config.Confinement.ExtraPerms {
		if p == nil {
			continue
		}

		seal.extraPerms[i] = new(sealedExtraPerm)
		seal.extraPerms[i].name = p.Path
		seal.extraPerms[i].perms = make(acl.Perms, 0, 3)
		if p.Read {
			seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Read)
		}
		if p.Write {
			seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Write)
		}
		if p.Execute {
			seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Execute)
		}
		seal.extraPerms[i].ensure = p.Ensure
	}

	// map sandbox config to bwrap
	if config.Confinement.Sandbox == nil {
		fmsg.VPrintln("sandbox configuration not supplied, PROCEED WITH CAUTION")

		// permissive defaults
		conf := &fst.SandboxConfig{
			UserNS:       true,
			Net:          true,
			NoNewSession: true,
			AutoEtc:      true,
		}
		// bind entries in /
		if d, err := a.os.ReadDir("/"); err != nil {
			return err
		} else {
			b := make([]*fst.FilesystemConfig, 0, len(d))
			for _, ent := range d {
				p := "/" + ent.Name()
				switch p {
				case "/proc":
				case "/dev":
				case "/tmp":
				case "/mnt":
				case "/etc":

				default:
					b = append(b, &fst.FilesystemConfig{Src: p, Write: true, Must: true})
				}
			}
			conf.Filesystem = append(conf.Filesystem, b...)
		}

		// hide nscd from sandbox if present
		nscd := "/var/run/nscd"
		if _, err := a.os.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
			conf.Override = append(conf.Override, nscd)
		}
		// bind GPU stuff
		if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
			conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
		}
		// opportunistically bind kvm
		conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/kvm", Device: true})

		config.Confinement.Sandbox = conf
	}
	seal.directWayland = config.Confinement.Sandbox.DirectWayland
	if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil {
		return err
	} else {
		seal.sys.bwrap = b
	}
	seal.sys.override = config.Confinement.Sandbox.Override
	if seal.sys.bwrap.SetEnv == nil {
		seal.sys.bwrap.SetEnv = make(map[string]string)
	}

	// open process state store
	// the simple store only starts holding an open file after first action
	// store activity begins after Start is called and must end before Wait
	seal.store = state.NewMulti(seal.RunDirPath)

	// initialise system interface with full uid
	seal.sys.I = system.New(seal.sys.user.uid)

	// pass through enablements
	seal.et = config.Confinement.Enablements

	// this method calls all share methods in sequence
	if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
		return err
	}

	// verbose log seal information
	fmsg.VPrintf("created application seal for uid %s (%s) groups: %v, command: %s",
		seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command)

	// seal app and release lock
	a.seal = seal
	a.ct = newAppCt(config)
	return nil
}