package client

import (
	"context"
	"strings"
	"sync"
	"time"

	"github.com/juju/errors"
	"github.com/pingcap/schrodinger/pkg/types"
)

// Failpoints defines tidb / tikv / pd failpoints
type Failpoints struct {
	sync.RWMutex
	// fail points name -> actions
	tidbFps map[string]string
	tikvFps map[string]string
	pdFps   map[string]string
}

func newFailPoints(failpoints []*types.Failpoint) *Failpoints {
	f := &Failpoints{
		tidbFps: make(map[string]string),
		tikvFps: make(map[string]string),
		pdFps:   make(map[string]string),
	}

	f.updateFailpoint(failpoints)
	return f
}

func (f *Failpoints) updateFailpoint(failpoints []*types.Failpoint) {
	f.Lock()
	defer f.Unlock()

	for _, fail := range failpoints {
		switch fail.Name {
		case TiKV:
			f.tikvFps = fail.Points
		case TiDB:
			f.tidbFps = fail.Points
		case PD:
			f.pdFps = fail.Points
		default:
			Errorf("failpoint name %s is not valid", fail.Name)
		}
	}
}

func (f *Failpoints) getFailpoint(kind, fail string) (action string, ok bool) {
	switch kind {
	case TiKV:
		f.RLock()
		action, ok = f.tikvFps[fail]
		f.RUnlock()
	case PD, TiDB:
	}

	return
}

func (f *Failpoints) setFailpoint(kind, fail, action string) error {
	switch kind {
	case TiKV:
		f.Lock()
		f.tikvFps[fail] = action
		f.Unlock()
	case PD, TiDB:
		return nil
	}

	return errors.NotFoundf("Service type :%s", kind)
}

func (f *Failpoints) randFailpoint(kind string) (fail string, action string, err error) {
	f.RLock()
	defer f.RUnlock()

	var failpoints map[string]string
	switch kind {
	case TiKV:
		failpoints = f.tikvFps
	case PD:
		failpoints = f.pdFps
	case TiDB:
		failpoints = f.tidbFps
	default:
	}

	for fail, action = range failpoints {
		return fail, action, nil
	}
	return "", "", errors.NotFoundf("service %s failpoint", kind)
}

func (c *Client) failTiKV(ctx context.Context, node *Node, fail, action string) error {
	Infof("start set faipoint %s to tikv %s", fail, node.Name())
	// Before any injection, make sure that the target is alive.
	node.start(ctx)

	Info("send failpoint to tikv")
	if err := node.failTiKV(ctx, fail, action); err != nil {
		return NewErrorf("set failpint %s to tikv %s failed: %v", fail, node.Name(), err)
	}

	Info("set failpoint end")
	return nil
}

func (c *Client) recoverTiKV(ctx context.Context, node *Node, fail string) error {
	Infof("start recover failpoint %s to tikv %s", fail, node.Name())

	err := node.recoverTiKV(ctx, fail)
	if err != nil {
		return NewErrorf("recover failpoint %s to tikv %s failed: %v", fail, node.Name())
	}

	// A failpoint may cause node panic, try to restart.
	if err := node.start(ctx); err != nil && !strings.Contains(err.Error(), "process from state [RUNNING]") {
		return NewErrorf("start node %s failed: %s", node.Name(), err)
	}

	return nil
}

// FailWithRecover injects failures to service according to the specified  name ,
// and recovers this service 'waitTime',
// Node is the unit of nemesis execution and contains the name and host of the service
func (c *Client) FailWithRecover(ctx context.Context, kind 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))
	}

	Info("[fail_with_recover] start")

	node := c.nemesis.selectNodeByKind(kind, false)
	if node == nil {
		return nil, NewErrorf("service %s not found", kind)
	}

	fail, action, err := c.failpoints.randFailpoint(kind)
	if err != nil {
		return nil, NewErrorf("fail to rand failpoint %v", err)
	}

	switch kind {
	case TiKV:
		if err := c.failTiKV(ctx, node, fail, action); err != nil {
			return node, errors.Trace(err)
		}

		// Some fail points may need long time, so do not recover immediately.
		sleep(ctx, waitTime)

		if err := c.recoverTiKV(ctx, node, fail); err != nil {
			return nil, errors.Trace(err)
		}
	case TiDB:
	case PD:
	default:
		return nil, NewErrorf("service %s not found", kind)
	}

	Info("[fail_with_recover] end")
	return node, nil
}

// SetFailpoint adds failpint to failNemesis
func (c *Client) SetFailpoint(kind, fail, action string) {
	if !c.isOnline() {
		Warnf("only mode online is permitted. the mode is %s", c.mode)
		return
	}

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

	c.failpoints.setFailpoint(kind, fail, action)
}
