package client

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/juju/errors"
)

// defines the operation
const (
	CPU       = "cpu"
	Memory    = "memory"
	Network   = "network"
	Disk      = "disk"
	Bandwidth = "bandwidth"
	Fs        = "fs"
	OpStart   = "start"
	OpStop    = "stop"
)

var (
	actions = map[string]Actions{
		CPU:       {"limit"},
		Memory:    {"limit"},
		Network:   {"delay", "fail", "loss"},
		Disk:      {"limit", "full", "fail"},
		Bandwidth: {"limit"},
		Fs:        {"fault", "faultall", "recover", "recoverall"},
	}
)

// Actions is a set of operation action
type Actions []string

// Contains reports whether action is within actions.
func (as Actions) Contains(action string) bool {
	for _, ac := range as {
		if ac == action {
			return true
		}
	}

	return false
}

func (c *Client) daemonAction(node *Node, operator, action, rate, operation, method string) error {
	op := &Operation{
		ContainerIP: node.podInfo.IP,
		ContainerID: node.podInfo.ContainerID,
		DirName:     node.podInfo.DataDir,
		Action:      action,
		Rate:        rate,
		Operation:   operation,
		Method:      method,
	}

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

	return errors.Trace(node.daemonAction(operator, c.conf.Experiment.DaemonPort, data))
}

func (c *Client) setPhysicalFailure(ctx context.Context, node *Node, operation, action, rate, method string) error {
	acs, ok := actions[operation]
	if !ok {
		return NewErrorf("[%s] not supported", operation)
	}
	if !acs.Contains(action) {
		return NewErrorf("[%s] action %s is not supported", operation, action)
	}

	Infof("[%s] start to apply node %s action %s, rate %s", operation, node.Name(), action, rate)

	Infof("[%s] send command to node", operation)
	if err := c.daemonAction(node, operation, action, rate, OpStart, method); err != nil {
		return NewErrorf("[%s] apply node %s action %s rate %s failed: %v", operation, node.Name(), action, rate, err)
	}

	Infof("[%s] apply action %s end", operation, action)
	return nil
}

func (c *Client) unSetPhysicalFailure(ctx context.Context, node *Node, operation, action, method string) error {
	acs, ok := actions[operation]
	if !ok {
		return NewErrorf("[%s] not supported", operation)
	}
	if !acs.Contains(action) {
		return NewErrorf("[%s] action %s is not supported", operation, action)
	}

	Infof("[%s] start to unset node %s action %s", operation, node.Name(), action)

	if err := c.daemonAction(node, operation, action, "", OpStop, method); err != nil {
		if strings.Contains(err.Error(), "resources do not exist") {
			Errorf("[%s] resources do not exist", operation)
		}
		return NewErrorf("[%s] unset node %s action %s failed: %v", operation, node.Name(), action, err)
	}

	Infof("[%s] unset action %s end", operation, action)
	return nil
}

func (c *Client) setWithRecover(ctx context.Context, kind, operation, action, rate, method string, waitTime time.Duration) (*Node, error) {
	Infof("start %s", operation)
	node := c.nemesis.selectNodeByKind(kind, false)
	if node == nil {
		return nil, NewErrorf("[operation: %s] service %s not found", operation, kind)
	}

	if err := c.setPhysicalFailure(ctx, node, operation, action, rate, method); err != nil {
		return nil, errors.Trace(err)
	}

	// wait
	sleep(ctx, waitTime)

	// recover
	if err := c.unSetPhysicalFailure(ctx, node, operation, action, method); err != nil {
		return nil, errors.Trace(err)
	}

	Infof("[operation: %s] end", operation)
	return node, nil
}

func (c *Client) specialSetWithRecover(ctx context.Context, operation, host, action string, egress, ingress,
	latency, latencyVariation int, waitTime time.Duration) error {
	conf := c.conf
	Infof("[operation: %s] start", operation)
	if err := c.specialSetPhysicalFailure(ctx, operation, host, action,
		egress, ingress, latency, latencyVariation, conf.Experiment.DaemonPort); err != nil {
		return errors.Trace(err)
	}

	// wait
	sleep(ctx, waitTime)

	// recover
	if err := c.specialUnSetPhysicalFailure(ctx, operation, host, action,
		conf.Experiment.DaemonPort); err != nil {
		return errors.Trace(err)
	}
	return nil
}

func (c *Client) specialDaemonAction2(node *Node, operator, action, methods string, errno uint32, random bool,
	pct uint32, path string, delay uint32, operation string) error {
	op := &Operation{
		ContainerIP: node.podInfo.IP,
		ContainerID: node.podInfo.ContainerID,
		Operation:   operation,
		DirName:     node.podInfo.DataDir,
		Action:      action,
		Methods:     methods,
		Errno:       errno,
		Random:      random,
		Pct:         pct,
		Path:        path,
		Delay:       delay,
	}

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

	return errors.Trace(node.daemonAction(operator, c.conf.Experiment.DaemonPort, data))
}

func (c *Client) specialSetPhysicalFailure2(ctx context.Context, node *Node, operation, action, methods string,
	errno uint32, random bool, pct uint32, path string, delay uint32) error {
	acs, ok := actions[operation]
	if !ok {
		return NewErrorf("[%s] not supported", operation)
	}
	if !acs.Contains(action) {
		return NewErrorf("[%s] action %s is not supported", operation, action)
	}

	Infof("[%s] start to apply node %s action %s, methods %s, errno: %d, random: %t, pct: %d, path: %s, delay: %d",
		operation, node.Name(), action, methods, errno, random, pct, path, delay)

	Infof("[%s] send command to node", operation)
	if err := c.specialDaemonAction2(node, operation, action, methods, errno, random, pct, path, delay, OpStart); err != nil {
		return NewErrorf("[%s] apply node %s action %s methods %s, errno: %d, random: %t, pct: %d, path: %s, delay: %d failed: %v",
			operation, node.Name(), action, methods, errno, random, pct, path, delay, err)
	}

	Infof("[%s] apply action %s end", operation, action)
	return nil
}

func (c *Client) specialUnSetPhysicalFailure2(ctx context.Context, node *Node, operation, action, methods, path string) error {
	acs, ok := actions[operation]
	if !ok {
		return NewErrorf("[%s] not supported", operation)
	}
	if !acs.Contains(action) {
		return NewErrorf("[%s] action %s is not supported", operation, action)
	}

	Infof("[%s] start to unset node %s action %s", operation, node.Name(), action)
	if err := c.specialDaemonAction2(node, operation, action, methods, 0, false, 0, path, 0, OpStop); err != nil {
		if strings.Contains(err.Error(), "resources do not exist") {
			Errorf("[%s] resources do not exist", operation)
		}
		return NewErrorf("[%s] unset node %s action %s failed: %v", operation, node.Name(), action, err)
	}

	Infof("[%s] unset action %s end", operation, action)
	return nil
}

func (c *Client) specialSetWithRecover2(ctx context.Context, kind, operation, action, methods string,
	errno uint32, random bool, pct uint32, path string, delay uint32, waitTime time.Duration) (*Node, error) {
	Infof("start %s", operation)
	node := c.nemesis.selectNodeByKind(kind, false)
	if node == nil {
		return nil, NewErrorf("[operation: %s] service %s not found", operation, kind)
	}

	if err := c.specialSetPhysicalFailure2(ctx, node, operation, action, methods, errno, random, pct, path, delay); err != nil {
		return nil, errors.Trace(err)
	}

	// wait
	sleep(ctx, waitTime)

	// recover
	action = "recover"
	if methods == "" {
		action = "recoverall"
	}
	if err := c.specialUnSetPhysicalFailure2(ctx, node, operation, action, methods, path); err != nil {
		return nil, errors.Trace(err)
	}

	Infof("[operation: %s] end", operation)
	return node, nil
}

func (c *Client) specialSetPhysicalFailure(ctx context.Context, operation, host, action string, egress, ingress,
	latency, latencyVariation, daemonPort int) error {
	acs, ok := actions[operation]
	if !ok {
		return NewErrorf("[%s] not supported", operation)
	}
	if !acs.Contains(action) {
		return NewErrorf("[%s] action %s is not supported", operation, action)
	}

	Infof("[%s] start to apply host %s action %s", operation, host, action)
	url := fmt.Sprintf("http://%s:%d/%s/%s", host, daemonPort, daemonPrefix, operation)

	if err := c.specialDaemonAction(ctx, url, host, action, egress, ingress, latency,
		latencyVariation, OpStart); err != nil {
		return NewErrorf("[%s] start to apply host %s action %s", operation, host, action)
	}

	Infof("[%s] apply host %s action %s end", operation, host, action)
	return nil
}

func (c *Client) specialUnSetPhysicalFailure(ctx context.Context, operation, host, action string,
	daemonPort int) error {
	acs, ok := actions[operation]
	if !ok {
		return NewErrorf("[%s] not supported", operation)
	}
	if !acs.Contains(action) {
		return NewErrorf("[%s] action %s is not supported", operation, action)
	}

	Infof("[%s] start to unset host %s action %s", operation, host, action)
	url := fmt.Sprintf("http://%s:%d/%s/%s", host, daemonPort, daemonPrefix, operation)

	if err := c.specialDaemonAction(ctx, url, host, action,
		0, 0, 0, 0, OpStop); err != nil {
		return NewErrorf("[%s] unset host %s action %s", operation, host, action)
	}

	Infof("[%s] unset host %s action %s end", operation, host, action)
	return nil
}

func (c *Client) specialDaemonAction(ctx context.Context, url, host, action string, egress, ingress,
	latency, latencyVariation int, operation string) error {
	op := &Operation{
		HostIP:           host,
		Operation:        operation,
		Action:           action,
		Egress:           egress,
		Ingress:          ingress,
		Latency:          latency,
		LatencyVariation: latencyVariation,
	}

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

	return errors.Trace(daemonAction(ctx, url, data))
}

// SetCPUWithRecover sets action on container cpu according to the name and stops action after `waitTime`,
// cpu action supports ["limit"],
// Node is the unit of nemesis execution.
//
// rate: the total available run-time within 1s
func (c *Client) SetCPUWithRecover(ctx context.Context, name, action, rate string, waitTime time.Duration) (*Node, error) {
	if !c.isOnline() {
		Warnf("only mode online is permitted. the mode is %s", c.mode)
		return nil, nil
	}

	if err := c.refreshConfig(); err != nil {
		Fatalf("init client config error %v", errors.ErrorStack(err))
	}

	return c.setWithRecover(ctx, name, CPU, action, rate, "", waitTime)
}

// SetMemoryWithRecover sets action to container memory according to the name and stops action after `waitTime`,
// memory action supports ["limit"],
// Node is the unit of nemesis execution.
//
// rate: how much rate to operation,can use K/M/G, default in B. This will be a limit on `memory.memsw.limit_in_bytes` and `memory.limit_in_bytes`
func (c *Client) SetMemoryWithRecover(ctx context.Context, name string, action string, rate string, waitTime time.Duration) (*Node, error) {
	if !c.isOnline() {
		Warnf("only mode online is permitted. the mode is %s", c.mode)
		return nil, nil
	}

	if err := c.refreshConfig(); err != nil {
		Fatalf("init client config error %v", errors.ErrorStack(err))
	}

	return c.setWithRecover(ctx, name, Memory, action, rate, "", waitTime)
}

// SetNetworkWithRecover sets action on container network according to the name and stops action after 'waitTime',
// network action supports ["delay", "fail", "loss"],
// Node is the unit of nemesis execution.
//
// rate: how much rate to operation. This will be operated by `tc` command.
func (c *Client) SetNetworkWithRecover(ctx context.Context, name string, action string, rate string, waitTime time.Duration) (*Node, error) {
	if !c.isOnline() {
		Warnf("only mode online is permitted. the mode is %s", c.mode)
		return nil, nil
	}

	if err := c.refreshConfig(); err != nil {
		Fatalf("init client config error %v", errors.ErrorStack(err))
	}

	return c.setWithRecover(ctx, name, Network, action, rate, "", waitTime)
}

// SetDiskWithRecover sets action on container disk according to the name and stops action after 'waitTime', wait 10 minutes at least.
// disk action supports ["limit", "full", "fail"],
// Node is the unit of nemesis execution.
//
// rate: how much rate to operation, support K/M/G, default is B. This will be a limit on `blkio.throttle.read_bps_device` and `blkio.throttle.write_bps_device`
func (c *Client) SetDiskWithRecover(ctx context.Context, name string, action string, rate string, waitTime time.Duration) (*Node, error) {
	if !c.isOnline() {
		Warnf("only mode online is permitted. the mode is %s", c.mode)
		return nil, nil
	}

	if err := c.refreshConfig(); err != nil {
		Fatalf("init client config error %v", errors.ErrorStack(err))
	}

	return c.setWithRecover(ctx, name, Disk, action, rate, "", waitTime)
}

// SetFsWithRecover sets action on container fs according to the name and stops action after 'waitTime', wait 10 minutes at least.
// fs action supports ["fault", "faultall"],
// Node is the unit of nemesis execution.
//
// methods: filesystem's operations, such as open, read, write, fsync and so on
// errno: syscall's return errno, such as EIO, ENOSPC and so on
// random: set true to random gen errno, vice versa
// pct: short for percent, failure injection percentage
// path: relative path (notice: relative to mountpoint), support simple regular expressions
// delay: the unit is microseconds
func (c *Client) SetFsWithRecover(ctx context.Context, name string, action,
	methods string, errno uint32, random bool, pct uint32, path string, delay uint32, waitTime time.Duration) (*Node, error) {
	if !c.isOnline() {
		Warnf("only mode online is permitted. the mode is %s", c.mode)
		return nil, nil
	}

	if err := c.refreshConfig(); err != nil {
		Fatalf("init client config error %v", errors.ErrorStack(err))
	}

	return c.specialSetWithRecover2(ctx, name, Fs, action, methods, errno, random, pct, path, delay, waitTime)
}

// SetHostBandwidthWithRecover set bandwidth on host according host ip and stops action after 'waitTime'.
// action supports ["limit"]
func (c *Client) SetHostBandwidthWithRecover(ctx context.Context, host string, action string,
	egress int, ingress int, latency int, latencyVariation int, waitTime time.Duration) error {
	if !c.isOnline() {
		Warnf("only mode online is permitted. the mode is %s", c.mode)
		return nil
	}

	if err := c.refreshConfig(); err != nil {
		Fatalf("init client config error %v", errors.ErrorStack(err))
	}

	return c.specialSetWithRecover(ctx, Bandwidth, host, action, egress, ingress, latency, latencyVariation, waitTime)
}
