package util

import (
	"fmt"
	"strings"
	"time"

	"github.com/juju/errors"
	"github.com/ngaut/log"
	agClient "github.com/pingcap/schrodinger/agent/client"
	"github.com/pingcap/schrodinger/agent/core"
	"github.com/pingcap/schrodinger/agent/daemon"
	"k8s.io/apimachinery/pkg/util/wait"
)

const (
	//AgentPort is agent port
	AgentPort = 32337
	// PersistDaemonPort is persist daemon port
	PersistDaemonPort = 8350
)

// GenClients gen agent clients
func GenClients(ips []string) []*agClient.Client {
	return genClients(ips, AgentPort)
}

func genClients(ips []string, port int) []*agClient.Client {
	var clients []*agClient.Client
	for _, ip := range ips {
		clients = append(clients, agClient.NewClient(fmt.Sprintf("%s:%d", ip, port)))
	}
	return clients
}

// GenPersistClients gens persist clients
func GenPersistClients(ips []string) []*agClient.Client {
	return genClients(ips, PersistDaemonPort)
}

// StartProcess starts process
func StartProcess(ip, process string) error {
	clients := GenClients([]string{ip})
	return errors.Trace(AgentDoRestart(clients, process))
}

// StartPresetProcess start
func StartPresetProcess(ip, process string) error {
	clients := GenPersistClients([]string{ip})
	return errors.Trace(AgentDoRestart(clients, process))
}

// StopProcess stops process
func StopProcess(ip, process string) error {
	clients := GenClients([]string{ip})
	return errors.Trace(AgentDoKill(clients, process))
}

// StopPresetProcess stops preset process
func StopPresetProcess(ip, process string) error {
	clients := GenPersistClients([]string{ip})
	return errors.Trace(AgentDoKill(clients, process))
}

// DownProcess down process
func DownProcess(ip, process string) error {
	clients := GenClients([]string{ip})
	return errors.Trace(AgentDoDown(clients, process))
}

// DownPresetProcess down Preset process
func DownPresetProcess(ip, process string) error {
	clients := GenPersistClients([]string{ip})
	return errors.Trace(AgentDoDown(clients, process))
}

// DownPresetProcess down Preset process for Wait
func DownPresetProcessForWait(ip, process string, timeout time.Duration) error {
	clients := GenPersistClients([]string{ip})
	return errors.Trace(AgentDoDownForWait(clients, process, timeout))
}

// AgentDoRestart does restart
func AgentDoRestart(clients []*agClient.Client, processName string) error {
	for _, cli := range clients {
		err := wait.Poll(20*time.Second, 5*time.Minute, func() (bool, error) {
			if err := cli.RestartProcess(processName); err != nil {
				if strings.Contains(err.Error(), "start process from state [RUNNING]") {
					return true, nil
				}

				log.Errorf("restart %s failed: %v, retry", processName, err)
				return false, nil
			}

			if err := checkProcessState(cli, processName, daemon.ProcStatRunning); err != nil {
				log.Errorf("check %s expect status %s failed, error: %v, retry", processName, daemon.ProcStatRunning, err)
				return false, nil
			}
			return true, nil
		})

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

// AgentDoKill kills agent
func AgentDoKill(clients []*agClient.Client, processName string) error {
	for _, cli := range clients {
		err := wait.Poll(10*time.Second, 1*time.Minute, func() (bool, error) {
			if err := cli.StopProcess(processName); err != nil {
				log.Errorf("stop %s failed: %v, retry", processName, err)
				return false, nil
			}

			if err := checkProcessState(cli, processName, daemon.ProcStatKilled); err != nil {
				return true, errors.Trace(err)
			}
			return true, nil
		})

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

// AgentDoDown downs agent
func AgentDoDown(clients []*agClient.Client, processName string) error {
	for _, cli := range clients {
		err := wait.Poll(10*time.Second, 1*time.Minute, func() (bool, error) {
			if err := cli.ShutdownProcess(processName); err != nil {
				log.Errorf("shutdown %s failed: %v, retry", processName, err)
				return false, nil
			}

			if err := checkProcessState(cli, processName, daemon.ProcStatStopped); err != nil {
				return true, errors.Trace(err)
			}
			return true, nil
		})

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

// AgentDoDown downs agent for wait
func AgentDoDownForWait(clients []*agClient.Client, processName string, timeout time.Duration) error {
	for _, cli := range clients {
		err := wait.Poll(10*time.Second, timeout, func() (bool, error) {
			if err := cli.ShutdownProcess(processName); err != nil {
				log.Errorf("shutdown %s failed: %v, retry", processName, err)
				return false, nil
			}

			if err := checkProcessStateForWait(cli, processName, daemon.ProcStatStopped, timeout); err != nil {
				return true, errors.Trace(err)
			}
			return true, nil
		})

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

// AgentExec exec agent shell
func AgentExec(ip string, command string) error {
	clients := GenClients([]string{ip})
	cmd := &core.ShellCommand{Command: command}
	return errors.Trace(agentExec(clients, cmd))
}

// PersistDaemonExec exec persist daemon
func PersistDaemonExec(ip string, command string) error {
	clients := GenPersistClients([]string{ip})
	cmd := &core.ShellCommand{Command: command}
	return errors.Trace(agentExec(clients, cmd))
}

func agentExec(clients []*agClient.Client, command *core.ShellCommand) error {
	for _, cli := range clients {
		data, err := cli.ExecCommand(command)
		if err != nil {
			log.Errorf("exec %+v failed: %v, retry", command, err)
			return errors.Trace(err)
		}

		log.Debug("exec shell return data is %s", string(data))
	}
	return nil
}

// ProcessState returns process state
func ProcessState(ip, processName string) (daemon.ProcessState, error) {
	cli := agClient.NewClient(fmt.Sprintf("%s:%d", ip, AgentPort))
	state, err := cli.ProcessState(processName)
	if err != nil {
		log.Errorf("get %s state failed: %v, retry", processName, err)
		return daemon.ProcStatUnknown, errors.Trace(err)
	}
	return state, nil
}

func checkProcessState(cli *agClient.Client, processName string, state daemon.ProcessState) error {
	err := wait.Poll(10*time.Second, 120*time.Second, func() (bool, error) {
		tmpState, err := cli.ProcessState(processName)
		if err != nil {
			log.Errorf("get %s state failed: %v, retry", processName, err)
			return false, nil
		}

		if tmpState != state {
			log.Errorf("get %s state: %s, expect state: %s, retry", processName,
				tmpState.String(), state.String())
			return false, nil
		}

		return true, nil
	})
	return errors.Trace(err)
}

func checkProcessStateForWait(cli *agClient.Client, processName string, state daemon.ProcessState, timeout time.Duration) error {
	err := wait.Poll(10*time.Second, timeout, func() (bool, error) {
		tmpState, err := cli.ProcessState(processName)
		if err != nil {
			log.Errorf("get %s state failed: %v, retry", processName, err)
			return false, nil
		}

		if tmpState != state {
			log.Errorf("get %s state: %s, expect state: %s, retry", processName,
				tmpState.String(), state.String())
			return false, nil
		}

		return true, nil
	})
	return errors.Trace(err)
}
