package client

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/juju/errors"
	"github.com/pingcap/schrodinger/agent/api"
	"github.com/pingcap/schrodinger/agent/core"
	"github.com/pingcap/schrodinger/agent/daemon"
	"github.com/pingcap/schrodinger/pkg/types"
)

// Client is used to communicate with the node server
type Client struct {
	addr   string
	client *http.Client
}

// NewClient creates the client.
func NewClient(addr string) *Client {
	return &Client{
		addr:   addr,
		client: http.DefaultClient,
	}
}

func (c *Client) getURLPrefix() string {
	return fmt.Sprintf("http://%s%s", c.addr, api.APIPrefix)
}

func (c *Client) doPost(suffix string, data []byte) ([]byte, error) {
	url := fmt.Sprintf("%s%s", c.getURLPrefix(), suffix)
	resp, err := c.client.Post(url, "application/json", bytes.NewBuffer(data))
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()
	data, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("%s:%s", resp.Status, data)
	}

	return data, nil
}

func (c *Client) doGet(suffix string) ([]byte, error) {
	url := fmt.Sprintf("%s%s", c.getURLPrefix(), suffix)
	resp, err := c.client.Get(url)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("%s:%s", resp.Status, data)
	}

	return data, nil
}

// StartProcess is to start up a process.
func (c *Client) StartProcess(name, binaryName, binaryAddr string, conf []string, restartLimit int32, restartInterval int32, timeout time.Duration, actions ...types.Action) error {
	di := &core.DaemonInfo{
		Name:            name,
		BinaryName:      binaryName,
		BinaryAddr:      binaryAddr,
		Conf:            conf,
		RestartLimit:    restartLimit,
		RestartInterval: restartInterval,
		Timeout:         timeout,
		Actions:         actions,
	}

	rawDaemonInfo, err := json.Marshal(di)
	if err != nil {
		return errors.Trace(err)
	}

	_, err = c.doPost(fmt.Sprintf("/process/%s/start", binaryName), rawDaemonInfo)
	return errors.Trace(err)
}

// KillProcessWithSignal kills a process with signal
func (c *Client) KillProcessWithSignal(name string, signal string) error {
	_, err := c.doPost(fmt.Sprintf("/process/%s/kill/%s", name, signal), nil)
	return errors.Trace(err)
}

// PauseProcess sends a SIGSTOP signal to pause a process.
func (c *Client) PauseProcess(name string) error {
	return c.KillProcessWithSignal(name, "19")
}

// ContinueProcess sends a SIGCONT signal to resume a process.
func (c *Client) ContinueProcess(name string) error {
	return c.KillProcessWithSignal(name, "18")
}

// StopProcess stops process.
func (c *Client) StopProcess(name string) error {
	_, err := c.doPost(fmt.Sprintf("/process/%s/stop", name), nil)
	return errors.Trace(err)
}

// RestartProcess restarts process.
func (c *Client) RestartProcess(name string) error {
	_, err := c.doPost(fmt.Sprintf("/process/%s/up", name), nil)
	return errors.Trace(err)
}

// ShutdownProcess kills process.
func (c *Client) ShutdownProcess(name string) error {
	_, err := c.doPost(fmt.Sprintf("/process/%s/down", name), nil)
	return errors.Trace(err)
}

// ProcessState returns process state.
func (c *Client) ProcessState(name string) (daemon.ProcessState, error) {
	data, err := c.doGet(fmt.Sprintf("/process/%s/state", name))
	if err != nil {
		return daemon.ProcStatUnknown, errors.Trace(err)
	}
	state, err := strconv.Atoi(strings.TrimSpace(string(data)))
	if err != nil {
		return daemon.ProcStatUnknown, errors.Trace(err)
	}
	return daemon.ProcessState(state), errors.Trace(err)
}

// ProcessDetail returns process detail.
func (c *Client) ProcessDetail(name string) (*daemon.RunStat, error) {
	data, err := c.doGet(fmt.Sprintf("/process/%s/detail", name))
	if err != nil {
		return nil, errors.Trace(err)
	}

	stat := &daemon.RunStat{}
	if err1 := json.Unmarshal(data, stat); err1 != nil {
		return nil, errors.Annotatef(err1, "fail to unmarshal %s", data)
	}

	return stat, errors.Trace(err)
}

// ExecCommand execs commands.
func (c *Client) ExecCommand(cmd *core.ShellCommand) (string, error) {
	cmdInfo, err := json.Marshal(cmd)
	if err != nil {
		return "", errors.Trace(err)
	}

	data, err := c.doPost("/shell", cmdInfo)

	return string(data), errors.Trace(err)
}
