package common

import (
	"bufio"
	"context"
	"crypto/tls"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/pingcap/log"
	"github.com/pkg/errors"
	"go.uber.org/zap"
)

var (
	pads = make([]byte, encGroupSize)
)

const (
	encGroupSize           = 8
	encMarker              = byte(0xFF)
	defaultKeyspaceLength  = 4
	httpTimeout            = 60
	keyspaceEtcdPathPrefix = "/keyspaces/tidb/"
)

type PdContext struct {
	tls       *tls.Config
	addresses []string
}

func NewPdContext(tls *tls.Config, addresses []string) *PdContext {
	return &PdContext{
		tls:       tls,
		addresses: addresses,
	}
}

func WriteFile(f *os.File, bytes []byte) error {
	writer := bufio.NewWriter(f)
	_, err := writer.Write(bytes)
	if err != nil {
		return err
	}
	err = writer.Flush()
	if err != nil {
		return err
	}
	return nil
}

// EncodeBytes guarantees the encoded value is in ascending order for comparison,
// encoding with the following rule:
//
//	[group1][marker1]...[groupN][markerN]
//	group is 8 bytes slice which is padding with 0.
//	marker is `0xFF - padding 0 count`
//
// For example:
//
//	[] -> [0, 0, 0, 0, 0, 0, 0, 0, 247]
//	[1, 2, 3] -> [1, 2, 3, 0, 0, 0, 0, 0, 250]
//	[1, 2, 3, 0] -> [1, 2, 3, 0, 0, 0, 0, 0, 251]
//	[1, 2, 3, 4, 5, 6, 7, 8] -> [1, 2, 3, 4, 5, 6, 7, 8, 255, 0, 0, 0, 0, 0, 0, 0, 0, 247]
//
// Refer: https://github.com/facebook/mysql-5.6/wiki/MyRocks-record-format#memcomparable-format
func EncodeBytes(data []byte) []byte {
	// Allocate more space to avoid unnecessary slice growing.
	// Assume that the byte slice size is about `(len(data) / encGroupSize + 1) * (encGroupSize + 1)` bytes,
	// that is `(len(data) / 8 + 1) * 9` in our implement.
	dLen := len(data)
	result := make([]byte, 0, (dLen/encGroupSize+1)*(encGroupSize+1))
	for idx := 0; idx <= dLen; idx += encGroupSize {
		remain := dLen - idx
		padCount := 0
		if remain >= encGroupSize {
			result = append(result, data[idx:idx+encGroupSize]...)
		} else {
			padCount = encGroupSize - remain
			result = append(result, data[idx:]...)
			result = append(result, pads[:padCount]...)
		}

		marker := encMarker - byte(padCount)
		result = append(result, marker)
	}
	return result
}

func GetRange(id uint32) ([]byte, []byte, []byte, []byte) {
	keyspaceIDBytes := make([]byte, defaultKeyspaceLength)
	nextKeyspaceIDBytes := make([]byte, defaultKeyspaceLength)
	binary.BigEndian.PutUint32(keyspaceIDBytes, id)
	binary.BigEndian.PutUint32(nextKeyspaceIDBytes, id+1)

	rawLeftBound := append([]byte{'r'}, keyspaceIDBytes[1:]...)
	rawRightBound := append([]byte{'r'}, nextKeyspaceIDBytes[1:]...)
	txnLeftBound := append([]byte{'x'}, keyspaceIDBytes[1:]...)
	txnRightBound := append([]byte{'x'}, nextKeyspaceIDBytes[1:]...)

	// todo log print encode(done)

	log.Info("[CHECK deleteRange]",
		zap.ByteString("rawLeftBound", rawLeftBound),
		zap.ByteString("rawRightBound", rawRightBound))
	log.Info("[CHECK deleteRange]",
		zap.ByteString("txnLeftBound", txnLeftBound),
		zap.ByteString("txnRightBound", txnRightBound))
	log.Info("[CHECK deleteRange(encode)]",
		zap.String("rawLeftBound", hex.EncodeToString(EncodeBytes(rawLeftBound))),
		zap.String("rawRightBound", hex.EncodeToString(EncodeBytes(rawRightBound))))
	log.Info("[CHECK deleteRange(encode)]",
		zap.String("txnLeftBound", hex.EncodeToString(EncodeBytes(txnLeftBound))),
		zap.String("txnRightBound", hex.EncodeToString(EncodeBytes(txnRightBound))))

	return rawLeftBound, rawRightBound, txnLeftBound, txnRightBound
}

func Substring(source string, start int, end int) string {
	var r = []rune(source)
	length := len(r)

	if start < 0 || end > length || start > end {
		return ""
	}

	if start == 0 && end == length {
		return source
	}

	var substring = ""
	for i := start; i < length; i++ {
		if i == end {
			substring += string(r[i])
			break
		}
	}

	return substring
}

// ComposeURL adds HTTP schema if missing and concats address with path
func ComposeURL(address, path string, secure bool) string {
	if strings.HasPrefix(address, "http://") || strings.HasPrefix(address, "https://") {
		return fmt.Sprintf("%s%s", address, path)
	}
	scheme := "http://"
	if secure {
		scheme = "https://"
	}
	return fmt.Sprintf("%s%s%s", scheme, address, path)
}

func DoRequestForNoNeedConfirm(ctx context.Context,
	pdctx *PdContext,
	route, method string,
	body io.Reader,
	isRun bool) ([]byte, error) {
	return DoRequest(ctx, pdctx, route, method, body, isRun, true)
}

func DoRequestForExecute(ctx context.Context,
	pdctx *PdContext,
	route, method string,
	body io.Reader,
	isRun bool,
	isSkipConfirm bool) ([]byte, error) {
	return DoRequest(ctx, pdctx, route, method, body, isRun, isSkipConfirm)
}

func DoRequest(ctx context.Context,
	pdctx *PdContext,
	route, method string,
	body io.Reader,
	isRun bool,
	isSkipConfirm bool) ([]byte, error) {
	var err error
	var req *http.Request
	var res *http.Response
	for _, addr := range pdctx.addresses {
		url := ComposeURL(addr, route, pdctx.tls != nil)
		log.Info("url", zap.String("url", url))
		req, err = http.NewRequestWithContext(ctx, method, url, body)
		if err != nil {
			return nil, err
		}
		if body != nil {
			req.Header.Set("Content-Type", "application/json")
		}

		var confirmMsg string
		if !isSkipConfirm {
			fmt.Println("Please confirm is't needs to be GC.(yes/no)")
			_, _ = fmt.Scanln(&confirmMsg)
		} else {
			confirmMsg = "yes"
		}

		if confirmMsg == "yes" {
			if isRun {
				log.Info("[REAL RUN]", zap.String("url", url), zap.String("method", method))
				httpClient := &http.Client{Timeout: time.Duration(httpTimeout) * time.Second}
				if pdctx.tls != nil {
					httpClient.Transport = &http.Transport{
						TLSClientConfig:   pdctx.tls,
						ForceAttemptHTTP2: true,
					}
				}
				res, err = httpClient.Do(req)
				if err != nil {
					log.Error("io error", zap.Error(err))
					return nil, err
				}
				if res.StatusCode != http.StatusOK {
					log.Error("response status is not 200", zap.Int("status", res.StatusCode))
					return nil, errors.Errorf("response status is not 200, statusCdoe:%d, status:%s", res.StatusCode, res.Status)
				}
				defer func() {
					err = res.Body.Close()
				}()

				if err == nil {
					bodyBytes, err := io.ReadAll(res.Body)
					if err != nil {
						log.Error("io error", zap.Error(err))
						return nil, err
					}
					return bodyBytes, err
				}
			} else {
				log.Info("confirm yes but '-isrun' is not set, skip the operator.")
			}
		} else {
			log.Info("confirm: NO.")
		}
	}
	return nil, err
}

func GetKeyspceEtcdPath(keyspaceId uint32) string {
	return fmt.Sprintf("%s%d/", keyspaceEtcdPathPrefix, keyspaceId)
}
