package helper

import (
	"errors"
	"io"
	"os/exec"
	"strconv"
	"sync"

	"git.ophivana.moe/security/fortify/helper/bwrap"
)

// BubblewrapName is the file name or path to bubblewrap.
var BubblewrapName = "bwrap"

type bubblewrap struct {
	// bwrap child file name
	name string

	// bwrap pipes
	p *pipes
	// returns an array of arguments passed directly
	// to the child process spawned by bwrap
	argF func(argsFD, statFD int) []string

	// pipes received by the child
	// nil if no pipes are required
	cp *pipes

	lock sync.RWMutex
	*exec.Cmd
}

func (b *bubblewrap) StartNotify(ready chan error) error {
	b.lock.Lock()
	defer b.lock.Unlock()

	if ready != nil && b.cp == nil {
		panic("attempted to start with status monitoring on a bwrap child initialised without pipes")
	}

	// Check for doubled Start calls before we defer failure cleanup. If the prior
	// call to Start succeeded, we don't want to spuriously close its pipes.
	if b.Cmd.Process != nil {
		return errors.New("exec: already started")
	}

	// prepare bwrap pipe and args
	if argsFD, _, err := b.p.prepareCmd(b.Cmd); err != nil {
		return err
	} else {
		b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(argsFD), "--", b.name)
	}

	// prepare child args and pipes if enabled
	if b.cp != nil {
		b.cp.ready = ready
		if argsFD, statFD, err := b.cp.prepareCmd(b.Cmd); err != nil {
			return err
		} else {
			b.Cmd.Args = append(b.Cmd.Args, b.argF(argsFD, statFD)...)
		}
	} else {
		b.Cmd.Args = append(b.Cmd.Args, b.argF(-1, -1)...)
	}

	if ready != nil {
		b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=1")
	} else if b.cp != nil {
		b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=0")
	} else {
		b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=-1")
	}

	if err := b.Cmd.Start(); err != nil {
		return err
	}

	// write bwrap args first
	if err := b.p.readyWriteArgs(); err != nil {
		return err
	}

	// write child args if enabled
	if b.cp != nil {
		if err := b.cp.readyWriteArgs(); err != nil {
			return err
		}
	}

	return nil
}

func (b *bubblewrap) Close() error {
	if b.cp == nil {
		panic("attempted to close bwrap child initialised without pipes")
	}

	return b.cp.closeStatus()
}

func (b *bubblewrap) Start() error {
	return b.StartNotify(nil)
}

func (b *bubblewrap) Unwrap() *exec.Cmd {
	return b.Cmd
}

// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
// Function argF returns an array of arguments passed directly to the child process.
func MustNewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) Helper {
	b, err := NewBwrap(conf, wt, name, argF)
	if err != nil {
		panic(err.Error())
	} else {
		return b
	}
}

// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
// Function argF returns an array of arguments passed directly to the child process.
func NewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) (Helper, error) {
	b := new(bubblewrap)

	if args, err := NewCheckedArgs(conf.Args()); err != nil {
		return nil, err
	} else {
		b.p = &pipes{args: args}
	}

	b.argF = argF
	b.name = name
	if wt != nil {
		b.cp = &pipes{args: wt}
	}
	b.Cmd = execCommand(BubblewrapName)

	return b, nil
}