package fail

import (
	"bytes"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"path"
	"strings"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/test-infra/sdk/resource"
)

const PDAPIPrefix = "/pd/api/v1"

// TiDB and TiKV use the same interface for failpoints:
// - GET /fail/: list all enabled failpoints
// - PUT /fail/{failpoint} action: enable the failpoint
// - DELETE /fail/{failpoint}: disable the failpoint

// FailClient is used to enable and disable failpoints in TiDB and TiKV dynamically.
//
// By default, TiDB and TiKV are not buit with failpoints support.
// For TiDB, you should run
// ```sh
// $ make failpoint-enable
// $ make
// $ make failpoint-disable
// ```
//
// For TiKV, you could run
// - `FAIL_POINT=1 make build` for a debug build.
// - `make fail_release` for a release build.
type FailClient struct {
	endpoints []*url.URL
	http      *http.Client

	RetryLimit int
}

func NewFailClient(tc resource.TiDBCluster, tp resource.TiDBCompType) *FailClient {
	var (
		addrType resource.TiDBAddrType
		prefix   string
	)
	switch tp {
	case resource.TiDB:
		addrType = resource.DBStatus
	case resource.TiKV:
		addrType = resource.KVStatus
	case resource.PD:
		addrType = resource.PDAddr
		prefix = PDAPIPrefix
	default:
		log.Fatalf("fail client does not support %s", tp)
	}
	peerURLs, err := tc.PeerURLs(addrType)
	// Handle the failpoint path which have special prefixes.
	for _, peerURL := range peerURLs {
		if prefix != "" {
			peerURL.Path = path.Join(peerURL.Path, prefix)
		}
	}
	if err != nil {
		log.Fatalf("get peer URLs err %v", err)
	}
	return newFailClient(peerURLs)
}

func newFailClient(endpoints []*url.URL) *FailClient {
	return &FailClient{endpoints, &http.Client{}, 3}
}

func (f *FailClient) Enable(failpoint string, action string) error {
	for _, api := range f.FailAPIs(failpoint) {
		_, err := f.doRequest(api, http.MethodPut, 0, withBody("", bytes.NewBufferString(action)))
		if err != nil {
			return errors.Trace(err)
		}
	}
	return nil
}

func (f *FailClient) Disable(failpoint string) error {
	for _, api := range f.FailAPIs(failpoint) {
		_, err := f.doRequest(api, http.MethodDelete, 0)
		if err != nil {
			return errors.Trace(err)
		}
	}
	return nil
}

func (f *FailClient) FailAPIs(failpoint string) []string {
	apis := make([]string, 0, len(f.endpoints))
	for _, endpoint := range f.endpoints {
		api := *endpoint
		api.Path = path.Join(endpoint.Path, "fail", failpoint)
		apis = append(apis, api.String())
	}
	return apis
}

func (f *FailClient) doRequest(url string, method string, trial int, opt ...*bodyOption) ([]byte, error) {
	if method == "" {
		method = http.MethodGet
	}
	b := &bodyOption{}
	if len(opt) != 0 {
		b = opt[0]
	}
	req, err := http.NewRequest(method, url, b.body)
	if err != nil {
		return nil, errors.Trace(err)
	}
	if b.contentType != "" {
		req.Header.Set("Content-Type", b.contentType)
	}
	resp, err := f.http.Do(req)
	if err != nil {
		if errmsg := err.Error(); f.RetryLimit > 0 && f.RetryLimit >= trial && (strings.Contains(errmsg, "no such host")) {
			time.Sleep(time.Duration(2*trial+1) * time.Second)
			return f.doRequest(url, method, trial+1, opt...)
		}
		return nil, errors.Trace(err)
	}
	defer resp.Body.Close()
	// TODO tidb failpoint http handler will return 204 status code
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
		msg, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, errors.Trace(err)
		}
		return nil, errors.Errorf("[%d] %s", resp.StatusCode, msg)
	}
	content, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return content, nil
}

type bodyOption struct {
	contentType string
	body        io.Reader
}

func withBody(contentType string, body io.Reader) *bodyOption {
	return &bodyOption{contentType, body}
}
