package main

import (
	"crypto/md5"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path"
	"strings"
	"syscall"
)

// CommandDelimiter is used when demarcating boundaries between
// functions
const CommandDelimiter = "───────────────────────────────────────────"

// CommandProcessStartSymbol is the fancy symbol we use to denote
// the start of a command
const CommandProcessStartSymbol = "►"

// CommandProcessStopSymbol is the fancy symbol we use to denote
// the end of a command
const CommandProcessStopSymbol = "■"

// ICommand is the interface for the Command class
type ICommand interface {
	// runs the command
	Run()
	// gets the id of the command
	GetID() string
	// get a pointer to the status channel
	GetStatus() *chan error
	// checks if the command is still running
	IsRunning() bool
	// checks if the command is valid
	IsValid() error
	// tells command to exit nicely
	SendInterrupt()
}

// InitCommand is for creating a new Command
func InitCommand(config *CommandConfig) *Command {
	commandHash := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s%v", config.Application, config.Arguments))))
	command := &Command{
		id: commandHash[:6],
	}
	command.config = config
	command.logger = InitLogger(&LoggerConfig{
		Name:   "command",
		Format: "production",
		Level:  config.LogLevel,
		AdditionalFields: &map[string]interface{}{
			"submodule": path.Base(fmt.Sprintf("%s", config.Application)),
		},
	})
	return command
}

// CommandConfig configures Command
type CommandConfig struct {
	Application string
	Arguments   []string
	Directory   string
	Environment []string
	LogLevel    LogLevel
}

// Command is the atomic command to run
type Command struct {
	id         string
	signal     chan os.Signal
	status     chan error
	run        chan error
	terminated chan error
	config     *CommandConfig
	cmd        *exec.Cmd
	logger     *Logger
	started    bool
	reported   bool
	stopped    bool
}

// GetID returns the command's ID, used for the execution group
// to report the running command
func (command *Command) GetID() string {
	return command.id
}

// GetStatus returns the command's status channel for the execution
// group to know when the command has terminated
func (command *Command) GetStatus() *chan error {
	return &command.status
}

// IsRunning allows callers to check if the command is running,
// the logic is tied into the Run()
func (command *Command) IsRunning() bool {
	return command.started && !command.stopped
}

// IsValid does some sanity checks on the provided
// application before we try to run it
func (command *Command) IsValid() error {
	application := command.config.Application
	if len(application) == 0 {
		return errors.New("no application was specified")
	}
	if path.IsAbs(application) {
		if _, err := os.Lstat(application); err != nil {
			if os.IsNotExist(err) {
				return fmt.Errorf("application at '%s' could not be found", application)
			}
			return err
		}
	}
	if _, err := exec.LookPath(application); err != nil {
		return err
	}
	return nil
}

// Run executes the command
func (command *Command) Run() {
	command.logger.Tracef("command[%s] is starting", command.id)
	command.handleInitialisation()
	go command.handleStart()
	go command.handleProcessLifecycle()
	select {
	case terminateCommand := <-command.terminated:
		command.handleStopped(terminateCommand)
	}
}

// SendInterrupt sends SIGINT to the command
func (command *Command) SendInterrupt() {
	command.logger.Tracef("SIGINT received by command %s", command.id)
	command.logger.Tracef("command[%v] status: %v/%v, msg: SIGINT >>> %v", command.id, command.started, command.terminated, &command.signal)
	command.signal <- syscall.SIGINT
}

func (command *Command) handleInitialisation() {
	if command.config == nil {
		panic("command.config needs to be defined before initialisation can be done")
	}
	command.signal = make(chan os.Signal, 0)
	command.status = make(chan error, 0)
	command.run = make(chan error, 0)
	command.terminated = make(chan error, 0)
	command.started = false
	command.reported = false
	command.stopped = false
	command.cmd = exec.Command(
		command.config.Application,
		command.config.Arguments...,
	)
	command.cmd.Dir = command.config.Directory
	command.cmd.Env = command.config.Environment
	for _, envvar := range os.Environ() {
		command.cmd.Env = append(command.cmd.Env, envvar)
	}
	// command.cmd.Env = append(command.config.Environment, "GOCACHE=on")
	command.cmd.Stderr = os.Stderr
	command.cmd.Stdout = os.Stdout
}

// handleProcessExited handles the exit status being sent by the process
func (command *Command) handleProcessExited(status error) error {
	command.logger.Tracef("process status: %v", command.cmd.ProcessState)
	command.terminated <- status
	return nil
}

func (command *Command) handleProcessLifecycle() error {
	for {
		select {
		case signal := <-command.signal: // caller -> Command: shut down please
			return command.handleSignalReceived(signal)
		case cmdRunStatus := <-command.run: // process -> Command: i'm done here
			return command.handleProcessExited(cmdRunStatus)
		default: // just run
			command.handleProcessReporting()
		}
	}
}

// handleProcessReporting handles the CLI reporting after a process
// has its PID reported
func (command *Command) handleProcessReporting() {
	if !command.reported {
		if command.cmd.Process != nil {
			command.logger.Infof(
				"'%v'\n%s %s pid:%v id:%s %s",
				strings.Join(command.config.Arguments, "', '"),
				CommandProcessStartSymbol,
				CommandDelimiter,
				command.cmd.Process.Pid,
				command.id,
				CommandProcessStartSymbol,
			)
			command.reported = true
		}
	}
}

// handleSignalReceived handles the signal received by the caller
func (command *Command) handleSignalReceived(signal os.Signal) error {
	command.logger.Tracef("caller sent signal %v", signal)
	command.terminated <- errors.New(signal.String())
	if err := command.cmd.Process.Signal(signal); err != nil {
		command.logger.Warn(err)
		return err
	}
	return nil
}

// handleStart starts the process
func (command *Command) handleStart() {
	command.started = true
	command.run <- command.cmd.Run()
}

// handleStopped processes the end of a command as reported
// by (*exec.Cmd).Run or (*exec.Cmd).Wait
func (command *Command) handleStopped(terminateCommand error) {
	command.logger.Tracef("command[%s] is exiting (%v)", command.id, terminateCommand)
	pid := -1
	if command.cmd.Process != nil {
		pid = command.cmd.Process.Pid
	}
	command.logger.Infof(
		"\n%s %s pid:%v id:%s %s",
		CommandProcessStopSymbol,
		CommandDelimiter,
		pid,
		command.id,
		CommandProcessStopSymbol,
	)
	command.stopped = true
	command.status <- terminateCommand
}
