package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	"net/url"
	"path"
	"strings"
	"sync"
	"time"

	"github.com/pingcap/kvproto/pkg/metapb"
	"github.com/pingcap/kvproto/pkg/pdpb"
	client "github.com/pingcap/schrodinger/pkg/test_client"
)

// RegionInfo records detail region info for api usage.
type RegionInfo struct {
	ID          uint64              `json:"id"`
	StartKey    string              `json:"start_key"`
	EndKey      string              `json:"end_key"`
	RegionEpoch *metapb.RegionEpoch `json:"epoch,omitempty"`
	Peers       []*metapb.Peer      `json:"peers,omitempty"`

	Leader          *metapb.Peer      `json:"leader,omitempty"`
	DownPeers       []*pdpb.PeerStats `json:"down_peers,omitempty"`
	PendingPeers    []*metapb.Peer    `json:"pending_peers,omitempty"`
	WrittenBytes    uint64            `json:"written_bytes,omitempty"`
	ReadBytes       uint64            `json:"read_bytes,omitempty"`
	ApproximateSize int64             `json:"approximate_size,omitempty"`
	ApproximateKeys int64             `json:"approximate_keys,omitempty"`
}

// RegionsInfo contains some regions with the detailed region info.
type RegionsInfo struct {
	Count   int           `json:"count"`
	Regions []*RegionInfo `json:"regions"`
}

// APIClient a timeout http client.
var APIClient http.Client

// ErrHTTPStatusCode the code is not 200.
var ErrHTTPStatusCode = errors.New("http: status code is not OK")

func init() {
	APIClient = http.Client{
		Timeout: time.Duration(20 * time.Second),
	}
}

func request(url string, method string, bodyType string, body io.Reader) (*http.Response, error) {
	if method == "" {
		method = http.MethodGet
	}
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", bodyType)
	return APIClient.Do(req)

}

// APIVersion the prefix with version of PD API
const APIVersion = "pd/api/v1"

// APIChecker is used to check the API status.
type APIChecker interface {
	Check([]string) error
}

// statusChecker checks the API required GET method.
type statusChecker struct {
	checkList []string
}

func newStatusChcker() APIChecker {
	checkList := []string{
		"cluster/status",
		"leader",
		"members",
		"config",
		"stores",
		"labels",
		"schedulers",
	}
	return &statusChecker{checkList}
}

const statusRetryTimes = 20

// Check implements the interface.
func (s *statusChecker) Check(pds []string) error {
	return s.retryableCheck(pds, statusRetryTimes)
}

func (s *statusChecker) retryableCheck(pds []string, retryTimes int) error {
	pdAddr := pds[rand.Intn(len(pds))]
	item := s.checkList[rand.Intn(len(s.checkList))]
	if !strings.HasPrefix(pdAddr, "http://") {
		pdAddr = "http://" + pdAddr
	}
	u, err := url.Parse(pdAddr)
	if err != nil {
		return err
	}
	u.Path = path.Join(u.Path, APIVersion, item)
	reqURL := u.String()
	for i := 0; i <= retryTimes; i++ {
		time.Sleep(time.Second)
		resp, rerr := request(reqURL, http.MethodGet, "text/html; charset=utf-8", nil)
		if rerr != nil {
			err = rerr
			client.Errorf("Status checker meet error: %s address: %s", err, reqURL)
			continue
		}
		defer resp.Body.Close()
		if resp.StatusCode != http.StatusOK {
			client.Errorf("Status checker got error status code: %d address: %s", resp.StatusCode, reqURL)
			err = ErrHTTPStatusCode
			continue
		}
		return nil
	}
	return attachError(reqURL, err)
}

func readJSON(r io.ReadCloser, data interface{}) error {
	defer r.Close()

	b, err := ioutil.ReadAll(r)
	if err != nil {
		return err
	}
	err = json.Unmarshal(b, data)
	if err != nil {
		return err
	}

	return nil
}

func attachError(apiAddr string, err error) error {
	if err == nil {
		return err
	}
	return fmt.Errorf("API address: %s , got %s", apiAddr, err)
}

// operatorCheckcer checks some API about operator.
type operatorChecker struct{}

func (o *operatorChecker) Check(pds []string) error {
	pdAddr := pds[rand.Intn(len(pds))]
	if !strings.HasPrefix(pdAddr, "http://") {
		pdAddr = "http://" + pdAddr
	}
	u, err := url.Parse(pdAddr)
	if err != nil {
		return err
	}
	originPath := u.Path
	u.Path = path.Join(originPath, APIVersion, "regions/key")
	reqURL := u.String()
	resp, err := request(reqURL, http.MethodGet, "text/html; charset=utf-8", nil)
	if err != nil {
		return attachError(reqURL, err)
	}
	defer resp.Body.Close()
	data := &RegionsInfo{}
	err = readJSON(resp.Body, data)
	if err != nil {
		return attachError(reqURL, err)
	}
	if data.Count == 0 {
		return nil
	}
	region := data.Regions[0]
	// post a operator
	var target uint64
	for _, p := range region.Peers {
		if p.GetId() != region.Leader.GetId() {
			target = p.GetStoreId()
		}
	}
	if target == 0 {
		return nil
	}
	u.Path = path.Join(originPath, APIVersion, "operators")
	reqURL = u.String()
	postData := make(map[string]interface{})
	postData["name"] = "transfer-leader"
	postData["region_id"] = region.ID
	postData["to_store_id"] = target
	postJSONData, err := json.Marshal(postData)
	if err != nil {
		return attachError(reqURL, err)
	}
	resp1, err := request(reqURL, http.MethodPost, "application/json", bytes.NewBuffer(postJSONData))
	if err != nil {
		return attachError(reqURL, err)
	}
	defer resp1.Body.Close()
	resp2, err := request(reqURL, http.MethodGet, "text/html; charset=utf-8", nil)
	if err != nil {
		return attachError(reqURL, err)
	}
	defer resp2.Body.Close()
	if resp2.StatusCode != http.StatusOK {
		return attachError(reqURL, ErrHTTPStatusCode)
	}
	return nil
}

func checkAPI(ctx context.Context, cli *client.Client, wg *sync.WaitGroup) {
	ticker := time.NewTicker(checkInterval)
	checkers := []APIChecker{newStatusChcker(), &operatorChecker{}}
	go func() {
		defer wg.Done()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				pds := cli.PDs()
				for _, c := range checkers {
					err := c.Check(pds)
					if err != nil {
						client.Fatalf("[pd test - check API] meets error: %s", err)
					}
				}
			}
		}
	}()
}
