package core

import (
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"sync"
	"time"

	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/schrodinger/agent/daemon"
	"github.com/pingcap/schrodinger/pkg/types"
	"github.com/pingcap/schrodinger/util"
)

const maxRetryCount = 5

// DaemonManager manages process under the agent
type DaemonManager struct {
	sync.RWMutex
	D map[string]*daemon.Daemon
}

// NewProcessManager returns a process manager
func NewProcessManager() *DaemonManager {
	return &DaemonManager{
		D: make(map[string]*daemon.Daemon),
	}
}

// DaemonInfo represents process startup information.
type DaemonInfo struct {
	Name            string         `json:"name"`
	BinaryAddr      string         `json:"binary_addr"`
	BinaryName      string         `json:"binary_name"`
	Conf            []string       `json:"conf"`
	RestartLimit    int32          `json:"restart-limit"`
	RestartInterval int32          `json:"restart-interval"`
	Timeout         time.Duration  `json:"timeout"`
	Actions         []types.Action `json:"actions"`
}

// Start adds a process in manager
func (p *DaemonManager) Start(info *DaemonInfo) error {
	p.RLock()
	d, ok := p.D[info.Name]
	p.RUnlock()
	if ok && d.ProcessState() == daemon.ProcStatRunning {
		return errors.AlreadyExistsf("process %s is running", info.Name)
	}

	ctx := context.Background()

	log.Infof("prepare agent action")
	for _, action := range info.Actions {
		err := action.Do(ctx)
		if err != nil {
			return errors.Trace(err)
		}
	}

	log.Infof("prepare agent action finished")

	d, err := jail(ctx, info)
	if err != nil {
		return errors.Trace(err)
	}

	d.Supervise(ctx)

	p.Lock()
	p.D[d.Name()] = d
	p.Unlock()

	return nil
}

// Down stops a process
func (p *DaemonManager) Down(name string) error {
	p.RLock()
	d, ok := p.D[name]
	p.RUnlock()
	if !ok {
		return errors.NotFoundf("process %s", name)
	}

	if !d.IsInSupersive() {
		return errors.Errorf("process %s is not in supervise, start it first", name)
	}

	err := d.Signal(daemon.SignalDown)
	return errors.Trace(err)
}

// Up restarts a process
func (p *DaemonManager) Up(name string) error {
	p.RLock()
	d, ok := p.D[name]
	p.RUnlock()
	if !ok {
		return errors.NotFoundf("process %s", name)
	}

	if !d.IsInSupersive() {
		return errors.Errorf("process %s is not in supervise, start it first", name)
	}

	return errors.Trace(d.Signal(daemon.SignalUp))
}

// Stop stops a process
func (p *DaemonManager) Stop(name string) error {
	p.RLock()
	d, ok := p.D[name]
	p.RUnlock()
	if !ok {
		return errors.NotFoundf("process %s", name)
	}

	if !d.IsInSupersive() {
		return errors.Errorf("process %s is not in supervise, start it first", name)
	}

	return errors.Trace(d.Signal(daemon.SignalKill))
}

// Kill kills a process
func (p *DaemonManager) Kill(name string, signal string) error {
	p.RLock()
	d, ok := p.D[name]
	p.RUnlock()
	if !ok {
		return errors.NotFoundf("process %s", name)
	}
	sigNum, err := strconv.Atoi(signal)
	if err != nil {
		return errors.NotSupportedf("signal %s", signal)
	}
	sig, err := daemon.SignalNumToSignal(sigNum)

	if err != nil {
		return errors.Trace(err)
	}

	if !d.IsInSupersive() {
		return errors.Errorf("process %s is not in supervise, start it first", name)
	}

	return errors.Trace(d.Signal(sig))
}

// GetState returns process state
func (p *DaemonManager) GetState(name string) (daemon.ProcessState, error) {
	var state daemon.ProcessState
	p.RLock()
	d, ok := p.D[name]
	p.RUnlock()
	if !ok {
		return state, errors.NotFoundf("process %s", name)
	}

	state = d.ProcessState()
	return state, nil
}

// GetDetail returns process runtime detail
func (p *DaemonManager) GetDetail(name string) (*daemon.RunStat, error) {
	p.RLock()
	d, ok := p.D[name]
	p.RUnlock()
	if !ok {
		return nil, errors.NotFoundf("process %s", name)
	}

	return d.GetRunningStat(), nil
}

//prepares run environment
func jail(ctx context.Context, info *DaemonInfo) (*daemon.Daemon, error) {
	// preset daemon dir; use daemon name as dir name, put binary under it
	// preset daemon binary version is the corresponding docker's version
	presetDaemonExist := false
	presetDaemonDir, err := filepath.Abs("preset_daemon")
	if err != nil {
		return nil, errors.Annotate(err, "filepath abs preset_daemon")
	}
	if util.IsDir(presetDaemonDir) {
		daemons, err1 := ioutil.ReadDir(presetDaemonDir)
		if err1 != nil {
			return nil, errors.Trace(err1)
		}

		for _, daemon := range daemons {
			if daemon.IsDir() && daemon.Name() == info.Name {
				presetDaemonExist = true
				break
			}
		}
	}

	var (
		cwd    string
		binDir string
	)
	if presetDaemonExist {
		log.Infof("%s is preset daemon", info.Name)
		cwd = filepath.Join(presetDaemonDir, info.Name)
		binDir = filepath.Join(cwd, "bin")
	} else {
		// prepare cwd dir
		cwd, err = filepath.Abs(info.Name)
		if err != nil {
			return nil, errors.Trace(err)
		}
		os.RemoveAll(cwd)
		os.MkdirAll(filepath.Dir(cwd), 0755)
		// prepare binary
		binDir = filepath.Join(cwd, "bin")
		err = util.RetryOnError(ctx, maxRetryCount, func() error {
			return util.InstallArchive(ctx, info.BinaryAddr, cwd)
		})
		if err != nil {
			return nil, errors.Trace(err)
		}
	}
	// prepare status dir
	statusDir := filepath.Join(cwd, "status")
	os.MkdirAll(statusDir, 0755)

	files, err := ioutil.ReadDir(binDir)
	if err != nil {
		return nil, errors.Trace(err)
	}

	isBinaryExist := false
	for _, file := range files {
		if file.IsDir() {
			continue
		}

		if file.Name() == info.BinaryName {
			isBinaryExist = true
			break
		}
	}
	if !isBinaryExist {
		return nil, errors.NotFoundf("binary %s", info.BinaryName)
	}

	config := &daemon.Config{
		Cwd:             cwd,
		Cmd:             fmt.Sprintf("bin/%s", info.BinaryName),
		StatusDir:       statusDir,
		Name:            info.Name,
		RestartLimit:    info.RestartLimit,
		RestartInterval: info.RestartInterval,
		Args:            info.Conf,
	}

	log.Infof("daemon %s config %+v", info.Name, config)

	d, err := daemon.New(config)
	return d, errors.Trace(err)
}

// ShellCommand represents shell commands.
type ShellCommand struct {
	Command string `json:"command"`
}

// ExecShell execs a shell command
func (p *DaemonManager) ExecShell(command *ShellCommand) (string, error) {
	cmd := exec.Command("/bin/sh", "-c", command.Command)
	stdoutStderr, err := cmd.CombinedOutput()
	return string(stdoutStderr), errors.Trace(err)
}
