package k8s

import (
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/schrodinger/agent/client"
	"github.com/pingcap/schrodinger/agent/daemon"
	"github.com/pingcap/schrodinger/pkg/types"
	"github.com/pingcap/schrodinger/util"
	"k8s.io/api/core/v1"
)

const (
	// AgentPort is agent port
	AgentPort = 32337
	// AgentImage is image for agent
	AgentImage = "hub.pingcap.net/schrodinger/agent:latest"
)

//Proc is an Proc
type Proc struct {
	sync.RWMutex
	agentCli *client.Client

	// Labels is labels k8s to have
	Labels      map[string]string
	Namespace   string
	Name        string
	ProcessName string
	BinaryName  string
	//URL is Proc binary download url
	URL string
	//Args is start args for Proc
	Args  string
	State PState

	Pod *v1.Pod

	RestartLimit    int32
	RestartInterval int32
	Timeout         time.Duration

	Env          []v1.EnvVar
	Resources    v1.ResourceRequirements
	VolumeMounts []v1.VolumeMount
	Volumes      []v1.Volume
	HostNetwork  bool
	Actions      []types.Action

	Image string
	Port  int32
}

// New creates a Proc
func New(namespace, name, binaryName string, labels map[string]string, url string, args string, restartLimit int32, restartInterval int32, timeout time.Duration, env []v1.EnvVar, resources v1.ResourceRequirements, image string) *Proc {
	if util.IsEmpty(image) {
		image = AgentImage
	}
	return &Proc{
		Namespace:       namespace,
		Name:            name,
		ProcessName:     name,
		BinaryName:      binaryName,
		Labels:          labels,
		URL:             url,
		Args:            args,
		RestartLimit:    restartLimit,
		RestartInterval: restartInterval,
		Timeout:         timeout,
		Env:             env,
		Resources:       resources,
		Image:           image,
		Port:            AgentPort,
		VolumeMounts:    []v1.VolumeMount{{Name: "timezone", ReadOnly: true, MountPath: "/etc/localtime"}},
		Volumes:         []v1.Volume{{Name: "timezone", VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/etc/localtime"}}}},
	}
}

// SetImage sets k8s image. By the way, the image must contians agent
func (p *Proc) SetImage(image string) {
	p.Image = image
}

// UseHostNetwork sets container use hostnetwork to run docker, can pull images as localhost/127.0.0.1 ..etc
func (p *Proc) UseHostNetwork() {
	p.HostNetwork = true
}

// SetDefaultValue sets default value
func (p *Proc) SetDefaultValue() {
	if !util.IsEmpty(p.ProcessName) {
		return
	}

	p.ProcessName = p.Name
}

// AddVolume adds volume for container
func (p *Proc) AddVolume(name string, readOnly bool, hostPath string, mountPath string) {
	p.Lock()
	defer p.Unlock()

	vm := v1.VolumeMount{
		Name:      name,
		ReadOnly:  readOnly,
		MountPath: mountPath,
	}
	p.VolumeMounts = append(p.VolumeMounts, vm)

	v := v1.Volume{
		Name:         name,
		VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: hostPath}},
	}

	p.Volumes = append(p.Volumes, v)
}

func (p *Proc) adjustArgs() []string {
	rawArgs := strings.Split(p.Args, " ")
	args := make([]string, 0, len(rawArgs))

	for _, arg := range rawArgs {
		arg = strings.TrimSpace(arg)
		if arg != "" {
			args = append(args, arg)
		}
	}

	return args
}

// SetAgentClient sets agent client
func (p *Proc) SetAgentClient() {
	if p.agentCli == nil {
		p.agentCli = client.NewClient(fmt.Sprintf("%s:%d", p.Pod.Status.PodIP, AgentPort))
	}
}

// StartProcess starts k8s with agent client
func (p *Proc) StartProcess() error {
	p.Lock()
	defer p.Unlock()
	return errors.Trace(p.agentCli.StartProcess(p.ProcessName, p.BinaryName, p.URL, p.adjustArgs(), p.RestartLimit, p.RestartInterval, p.Timeout, p.Actions...))
}

// StopProcess stops k8s with agent client
func (p *Proc) StopProcess() error {
	p.Lock()
	defer p.Unlock()
	return errors.Trace(p.agentCli.StopProcess(p.ProcessName))
}

// RestartProcess restarts k8s with agent client
func (p *Proc) RestartProcess() error {
	p.Lock()
	defer p.Unlock()
	return errors.Trace(p.agentCli.RestartProcess(p.ProcessName))
}

// UpdateState updates the state of k8s
func (p *Proc) UpdateState(s PState) {
	p.Lock()
	defer p.Unlock()
	p.updateState(s)
}

// CanStop checks if the proc can stop
func (p *Proc) CanStop() bool {
	switch p.State {
	case ProcRunning, ProcRestarting, ProcStarting, ProcPaused:
		return true
	default:
		return false
	}
}

func (p *Proc) updateState(s PState) {
	p.State = s
}

//GetState returns Proc state
func (p *Proc) GetState() PState {
	p.Lock()
	defer p.Unlock()

	p.State = p.getState()
	return p.State
}

// IsNotFound checks the state of proc is not found
func (p *Proc) IsNotFound() (bool, error) {
	if p.agentCli == nil {
		return false, errors.New("agent client is nil")
	}

	_, err := p.agentCli.ProcessState(p.Name)
	if err != nil && strings.Contains(err.Error(), "not found") {
		return true, nil
	}

	return false, errors.Trace(err)
}

func (p *Proc) getState() PState {
	if p.agentCli == nil {
		return ProcUnknown
	}

	log.Debugf("process %+v", p)

	proState, err := p.agentCli.ProcessState(p.ProcessName)
	if err != nil {
		log.Errorf("get namespace %s case %s process %s failed: %v", p.Namespace, p.ProcessName, p.BinaryName, err)
		return ProcTimeout
	}

	switch proState {
	case daemon.ProcStatStopped:
		return ProcStopped
	case daemon.ProcStatStarting:
		return ProcStarting
	case daemon.ProcStatRunning:
		return ProcRunning
	case daemon.ProcStatRestarting:
		return ProcRestarting
	case daemon.ProcStatStopping:
		return ProcStopping
	case daemon.ProcStatKilling:
		return ProcKilling
	case daemon.ProcStatTerminating:
		return ProcTerminating
	case daemon.ProcStatNormalExited:
		return ProcNormalExited
	case daemon.ProcStatErrorExited:
		return ProcErrorExited
	case daemon.ProcStatKilled:
		return ProcKilled
	case daemon.ProcStatFatal:
		return ProcFatal
	case daemon.ProcStatUnknown:
		return ProcUnknown
	default:
		return ProcUnknown
	}
}

// IsError returns true if k8s had error
func (p *Proc) IsError() bool {
	p.Lock()
	defer p.Unlock()

	p.State = p.getState()
	return p.isError()
}

func (p *Proc) isError() bool {
	switch p.State {
	case ProcCreatePodError, ProcFatal, ProcKilled, ProcKilling, ProcStartError, ProcUnknown, ProcTimeout, ProcErrorExited:
		return true
	default:
		return false
	}
}

// Finished returns true if case if finished
func (p *Proc) Finished() bool {
	p.RLock()
	defer p.RUnlock()
	p.State = p.getState()
	return p.finished()
}

func (p *Proc) finished() bool {
	return p.State == ProcDestroyed
}
