package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"sync"
	"syscall"

	"hilbish/util"

	rt "github.com/arnodel/golua/runtime"
)

var jobs *jobHandler
var jobMetaKey = rt.StringValue("hshjob")

// #type
// #interface jobs
// #property cmd The user entered command string for the job.
// #property running Whether the job is running or not.
// #property id The ID of the job in the job table
// #property pid The Process ID
// #property exitCode The last exit code of the job.
// #property stdout The standard output of the job. This just means the normal logs of the process.
// #property stderr The standard error stream of the process. This (usually) includes error messages of the job.
// The Job type describes a Hilbish job.
type job struct {
	cmd string
	running bool
	id int
	pid int
	exitCode int
	once bool
	args []string
	// save path for a few reasons, one being security (lmao) while the other
	// would just be so itll be the same binary command always (path changes)
	path string
	handle *exec.Cmd
	cmdout io.Writer
	cmderr io.Writer
	stdout *bytes.Buffer
	stderr *bytes.Buffer
	ud *rt.UserData
}

func (j *job) start() error {
	if j.handle == nil || j.once {
		// cmd cant be reused so make a new one
		cmd := exec.Cmd{
			Path: j.path,
			Args: j.args,
		}
		j.setHandle(&cmd)
	}
	// bgProcAttr is defined in execfile_<os>.go, it holds a procattr struct
	// in a simple explanation, it makes signals from hilbish (sigint)
	// not go to it (child process)
	j.handle.SysProcAttr = bgProcAttr
	// reset output buffers
	j.stdout.Reset()
	j.stderr.Reset()
	// make cmd write to both standard output and output buffers for lua access
	j.handle.Stdout = io.MultiWriter(j.cmdout, j.stdout)
	j.handle.Stderr = io.MultiWriter(j.cmderr, j.stderr)

	if !j.once {
		j.once = true
	}

	err := j.handle.Start()
	proc := j.getProc()

	j.pid = proc.Pid
	j.running = true

	hooks.Emit("job.start", rt.UserDataValue(j.ud))

	return err
}

func (j *job) stop() {
	// finish will be called in exec handle
	proc := j.getProc()
	if proc != nil {
		proc.Kill()
	}
}

func (j *job) finish() {
	j.running = false
	hooks.Emit("job.done", rt.UserDataValue(j.ud))
}

func (j *job) wait() {
	j.handle.Wait()
}

func (j *job) setHandle(handle *exec.Cmd) {
	j.handle = handle
	j.args = handle.Args
	j.path = handle.Path
	if handle.Stdout != nil {
		j.cmdout = handle.Stdout
	}
	if handle.Stderr != nil {
		j.cmderr = handle.Stderr
	}
}

func (j *job) getProc() *os.Process {
	handle := j.handle
	if handle != nil {
		return handle.Process
	}

	return nil
}

// #interface jobs
// #member
// start()
// Starts running the job.
func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	if err := c.Check1Arg(); err != nil {
		return nil, err
	}

	j, err := jobArg(c, 0)
	if err != nil {
		return nil, err
	}

	if !j.running {
		err := j.start()
		exit := handleExecErr(err)
		j.exitCode = int(exit)
		j.finish()
	}

	return c.Next(), nil
}

// #interface jobs
// #member
// stop()
// Stops the job from running.
func luaStopJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	if err := c.Check1Arg(); err != nil {
		return nil, err
	}

	j, err := jobArg(c, 0)
	if err != nil {
		return nil, err
	}

	if j.running {
		j.stop()
		j.finish()
	}

	return c.Next(), nil
}

// #interface jobs
// #member
// foreground()
// Puts a job in the foreground. This will cause it to run like it was
// executed normally and wait for it to complete.
func luaForegroundJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	if err := c.Check1Arg(); err != nil {
		return nil, err
	}

	j, err := jobArg(c, 0)
	if err != nil {
		return nil, err
	}

	if !j.running {
		return nil, errors.New("job not running")
	}

	// lua code can run in other threads and goroutines, so this exists
	jobs.foreground = true
	// this is kinda funny
	// background continues the process incase it got suspended
	err = j.background()
	if err != nil {
		return nil, err
	}

	err = j.foreground()
	if err != nil {
		return nil, err
	}
	jobs.foreground = false

	return c.Next(), nil
}

// #interface jobs
// #member
// background()
// Puts a job in the background. This acts the same as initially running a job.
func luaBackgroundJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	if err := c.Check1Arg(); err != nil {
		return nil, err
	}

	j, err := jobArg(c, 0)
	if err != nil {
		return nil, err
	}

	if !j.running {
		return nil, errors.New("job not running")
	}

	err = j.background()
	if err != nil {
		return nil, err
	}

	return c.Next(), nil
}

type jobHandler struct {
	jobs map[int]*job
	latestID int
	foreground bool // if job currently in the foreground
	mu *sync.RWMutex
}

func newJobHandler() *jobHandler {
	return &jobHandler{
		jobs: make(map[int]*job),
		latestID: 0,
		mu: &sync.RWMutex{},
	}
}

func (j *jobHandler) add(cmd string, args []string, path string) *job {
	j.mu.Lock()
	defer j.mu.Unlock()

	j.latestID++
	jb := &job{
		cmd: cmd,
		running: false,
		id: j.latestID,
		args: args,
		path: path,
		cmdout: os.Stdout,
		cmderr: os.Stderr,
		stdout: &bytes.Buffer{},
		stderr: &bytes.Buffer{},
	}
	jb.ud = jobUserData(jb)

	j.jobs[j.latestID] = jb
	hooks.Emit("job.add", rt.UserDataValue(jb.ud))

	return jb
}

func (j *jobHandler) getLatest() *job {
	j.mu.RLock()
	defer j.mu.RUnlock()

	return j.jobs[j.latestID]
}

func (j *jobHandler) disown(id int) error {
	j.mu.RLock()
	if j.jobs[id] == nil {
		return errors.New("job doesnt exist")
	}
	j.mu.RUnlock()

	j.mu.Lock()
	delete(j.jobs, id)
	j.mu.Unlock()

	return nil
}

func (j *jobHandler) stopAll() {
	j.mu.RLock()
	defer j.mu.RUnlock()

	for _, jb := range j.jobs {
		// on exit, unix shell should send sighup to all jobs
		if jb.running {
			proc := jb.getProc()
			proc.Signal(syscall.SIGHUP)
			jb.wait() // waits for program to exit due to sighup
		}
	}
}

// #interface jobs
// background job management
/*
Manage interactive jobs in Hilbish via Lua.

Jobs are the name of background tasks/commands. A job can be started via
interactive usage or with the functions defined below for use in external runners. */
func (j *jobHandler) loader(rtm *rt.Runtime) *rt.Table {
	jobMethods := rt.NewTable()
	jFuncs := map[string]util.LuaExport{
		"stop": {luaStopJob, 1, false},
		"start": {luaStartJob, 1, false},
		"foreground": {luaForegroundJob, 1, false},
		"background": {luaBackgroundJob, 1, false},
	}
	util.SetExports(l, jobMethods, jFuncs)

	jobMeta := rt.NewTable()
	jobIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
		j, _ := jobArg(c, 0)

		arg := c.Arg(1)
		val := jobMethods.Get(arg)

		if val != rt.NilValue {
			return c.PushingNext1(t.Runtime, val), nil
		}

		keyStr, _ := arg.TryString()

		switch keyStr {
			case "cmd": val = rt.StringValue(j.cmd)
			case "running": val = rt.BoolValue(j.running)
			case "id": val = rt.IntValue(int64(j.id))
			case "pid": val = rt.IntValue(int64(j.pid))
			case "exitCode": val = rt.IntValue(int64(j.exitCode))
			case "stdout": val = rt.StringValue(string(j.stdout.Bytes()))
			case "stderr": val = rt.StringValue(string(j.stderr.Bytes()))
		}

		return c.PushingNext1(t.Runtime, val), nil
	}

	jobMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(jobIndex, "__index", 2, false)))
	l.SetRegistry(jobMetaKey, rt.TableValue(jobMeta))

	jobFuncs := map[string]util.LuaExport{
		"all": {j.luaAllJobs, 0, false},
		"last": {j.luaLastJob, 0, false},
		"get": {j.luaGetJob, 1, false},
		"add": {j.luaAddJob, 3, false},
		"disown": {j.luaDisownJob, 1, false},
	}

	luaJob := rt.NewTable()
	util.SetExports(rtm, luaJob, jobFuncs)

	return luaJob
}

func jobArg(c *rt.GoCont, arg int) (*job, error) {
	j, ok := valueToJob(c.Arg(arg))
	if !ok {
		return nil, fmt.Errorf("#%d must be a job", arg + 1)
	}

	return j, nil
}

func valueToJob(val rt.Value) (*job, bool) {
	u, ok := val.TryUserData()
	if !ok {
		return nil, false
	}

	j, ok := u.Value().(*job)
	return j, ok
}

func jobUserData(j *job) *rt.UserData {
	jobMeta := l.Registry(jobMetaKey)
	return rt.NewUserData(j, jobMeta.AsTable())
}

// #interface jobs
// get(id) -> @Job
// Get a job object via its ID.
// --- @param id number
// --- @returns Job
func (j *jobHandler) luaGetJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	j.mu.RLock()
	defer j.mu.RUnlock()

	if err := c.Check1Arg(); err != nil {
		return nil, err
	}
	jobID, err := c.IntArg(0)
	if err != nil {
		return nil, err
	}

	job := j.jobs[int(jobID)]
	if job == nil {
		return c.Next(), nil
	}

	return c.PushingNext(t.Runtime, rt.UserDataValue(job.ud)), nil
}

// #interface jobs
// add(cmdstr, args, execPath)
// Creates a new job. This function does not run the job. This function is intended to be
// used by runners, but can also be used to create jobs via Lua. Commanders cannot be ran as jobs.
// #param cmdstr string String that a user would write for the job
// #param args table Arguments for the commands. Has to include the name of the command.
// #param execPath string Binary to use to run the command. Needs to be an absolute path.
/*
#example
hilbish.jobs.add('go build', {'go', 'build'}, '/usr/bin/go')
#example
*/
func (j *jobHandler) luaAddJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	if err := c.CheckNArgs(3); err != nil {
		return nil, err
	}
	cmd, err := c.StringArg(0)
	if err != nil {
		return nil, err
	}
	largs, err := c.TableArg(1)
	if err != nil {
		return nil, err
	}
	execPath, err := c.StringArg(2)
	if err != nil {
		return nil, err
	}

	var args []string
	util.ForEach(largs, func(k rt.Value, v rt.Value) {
		if v.Type() == rt.StringType {
			args = append(args, v.AsString())
		}
	})

	jb := j.add(cmd, args, execPath)

	return c.PushingNext1(t.Runtime, rt.UserDataValue(jb.ud)), nil
}

// #interface jobs
// all() -> table[@Job]
// Returns a table of all job objects.
// #returns table[Job]
func (j *jobHandler) luaAllJobs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	j.mu.RLock()
	defer j.mu.RUnlock()

	jobTbl := rt.NewTable()
	for id, job := range j.jobs {
		jobTbl.Set(rt.IntValue(int64(id)), rt.UserDataValue(job.ud))
	}

	return c.PushingNext1(t.Runtime, rt.TableValue(jobTbl)), nil
}

// #interface jobs
// disown(id)
// Disowns a job. This simply deletes it from the list of jobs without stopping it.
// #param id number
func (j *jobHandler) luaDisownJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	if err := c.Check1Arg(); err != nil {
		return nil, err
	}
	jobID, err := c.IntArg(0)
	if err != nil {
		return nil, err
	}

	err = j.disown(int(jobID))
	if err != nil {
		return nil, err
	}

	return c.Next(), nil
}

// #interface jobs
// last() -> @Job
// Returns the last added job to the table.
// #returns Job
func (j *jobHandler) luaLastJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
	j.mu.RLock()
	defer j.mu.RUnlock()

	job := j.jobs[j.latestID]
	if job == nil { // incase we dont have any jobs yet
		return c.Next(), nil
	}

	return c.PushingNext1(t.Runtime, rt.UserDataValue(job.ud)), nil
}
