package astikit

import (
	"context"
	"fmt"
	"os/exec"
	"strings"
	"sync"
)

// Statuses
const (
	ExecStatusCrashed = "crashed"
	ExecStatusRunning = "running"
	ExecStatusStopped = "stopped"
)

// ExecHandler represents an object capable of handling the execution of a cmd
type ExecHandler struct {
	cancel  context.CancelFunc
	ctx     context.Context
	err     error
	o       sync.Once
	stopped bool
}

// Status returns the cmd status
func (h *ExecHandler) Status() string {
	if h.ctx.Err() != nil {
		if h.stopped || h.err == nil {
			return ExecStatusStopped
		}
		return ExecStatusCrashed
	}
	return ExecStatusRunning
}

// Stop stops the cmd
func (h *ExecHandler) Stop() {
	h.o.Do(func() {
		h.cancel()
		h.stopped = true
	})
}

// ExecCmdOptions represents exec options
type ExecCmdOptions struct {
	Args       []string
	CmdAdapter func(cmd *exec.Cmd, h *ExecHandler) error
	Name       string
	StopFunc   func(cmd *exec.Cmd) error
}

// ExecCmd executes a cmd
// The process will be stopped when the worker stops
func ExecCmd(w *Worker, o ExecCmdOptions) (h *ExecHandler, err error) {
	// Create handler
	h = &ExecHandler{}
	h.ctx, h.cancel = context.WithCancel(w.Context())

	// Create command
	cmd := exec.Command(o.Name, o.Args...)

	// Adapt command
	if o.CmdAdapter != nil {
		if err = o.CmdAdapter(cmd, h); err != nil {
			err = fmt.Errorf("astikit: adapting cmd failed: %w", err)
			return
		}
	}

	// Start
	w.Logger().Infof("astikit: starting %s", strings.Join(cmd.Args, " "))
	if err = cmd.Start(); err != nil {
		err = fmt.Errorf("astikit: executing %s: %w", strings.Join(cmd.Args, " "), err)
		return
	}

	// Handle context
	go func() {
		// Wait for context to be done
		<-h.ctx.Done()

		// Get stop func
		f := func() error { return cmd.Process.Kill() }
		if o.StopFunc != nil {
			f = func() error { return o.StopFunc(cmd) }
		}

		// Stop
		if err := f(); err != nil {
			w.Logger().Error(fmt.Errorf("astikit: stopping cmd failed: %w", err))
			return
		}
	}()

	// Execute in a task
	w.NewTask().Do(func() {
		h.err = cmd.Wait()
		h.cancel()
		w.Logger().Infof("astikit: status is now %s for %s", h.Status(), strings.Join(cmd.Args, " "))
	})
	return
}
