package util

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/gob"
	"fmt"
	"go/build"
	"io"
	"io/ioutil"
	"math/rand"
	"net/url"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
	"unicode"

	"github.com/Kenshin/curl"
	_ "github.com/go-sql-driver/mysql" // mysql driver
	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/schrodinger/config"
)

// Version information.
var (
	BuildTS   = "None"
	BuildHash = "None"

	alphabet = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

	GoGithubPath = build.Default.GOPATH
	pingcap      = fmt.Sprintf("%s/src/github.com/pingcap", GoGithubPath)
	ImagePrefix  = "hub.pingcap.net/schrodinger"
)

// Git type
const (
	GitHash   = "hash"
	GitTag    = "tag"
	GitBranch = "branch"
)

// Variables for generates uniq id
var (
	lock sync.Mutex
	uid  int64 = 1
)

const (
	// TimeFormat defines the format for time string
	TimeFormat = "2006-01-02 15:04:05"

	// hashLength defines hash Length
	hashLength = 40
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

// PrintInfo prints the octopus version information
func PrintInfo() {
	fmt.Println("Git Commit Hash:", BuildHash)
	fmt.Println("UTC Build Time: ", BuildTS)
}

// IsFileExist returns true if the file exists.
func IsFileExist(name string) bool {
	_, err := os.Stat(name)
	return err == nil
}

// IsFile return true if path is a regular file
func IsFile(path string) bool {
	f, err := os.Stat(path)
	if err != nil {
		return false
	}
	if m := f.Mode(); !m.IsDir() && m.IsRegular() && m&400 != 0 {
		return true
	}
	return false
}

// IsDir return true if path is a dir
func IsDir(path string) bool {
	f, err := os.Stat(path)
	if err != nil {
		return false
	}
	if m := f.Mode(); m.IsDir() && m&400 != 0 {
		return true
	}
	return false
}

// Wget downloads a string URL to the dest directory and returns the file path.
// SKips if the file already exists.
func Wget(ctx context.Context, rawURL string, dest string) (string, error) {
	u, err := url.Parse(rawURL)
	if err != nil {
		return "", err
	}

	if len(dest) == 0 {
		dest = "."
	}

	fileName := path.Base(u.Path)
	filePath := path.Join(dest, fileName)
	if IsFileExist(filePath) {
		return filePath, nil
	}

	os.MkdirAll(dest, 0755)
	err = exec.CommandContext(ctx, "wget", "--tries", "20", "--waitretry", "60",
		"--retry-connrefused", "--dns-timeout", "60", "--connect-timeout", "60",
		"--read-timeout", "60", "--directory-prefix", dest, rawURL).Run()
	return filePath, errors.Annotatef(err, "wget %s to %s", rawURL, dest)
}

// InstallArchive downloads the URL and extracts the archive to the dest diretory.
// Supports zip, and tarball.
func InstallArchive(ctx context.Context, rawURL string, dest string) error {
	tmpDir, err := ioutil.TempDir(".", "")
	if err != nil {
		return err
	}
	defer os.RemoveAll(tmpDir)

	var name string
	if strings.HasPrefix(rawURL, "file://") {
		name = strings.Trim(rawURL, "file://")
	} else {
		if name, err = Wget(ctx, rawURL, "/tmp/chaos"); err != nil {
			return err
		}
	}

	if strings.HasSuffix(name, ".zip") {
		err = exec.CommandContext(ctx, "unzip", "-d", tmpDir, name).Run()
	} else {
		err = exec.CommandContext(ctx, "tar", "-xf", name, "-C", tmpDir).Run()
	}
	if err != nil {
		return errors.Annotate(err, "decompression error")
	}

	if dest, err = filepath.Abs(dest); err != nil {
		return err
	}

	os.RemoveAll(dest)
	os.MkdirAll(path.Dir(dest), 0755)

	var files []os.FileInfo
	if files, err = ioutil.ReadDir(tmpDir); err != nil {
		return err
	}

	for _, file := range files {
		if file.IsDir() {
			if file.Name() == "bin" {
				return os.Rename(tmpDir, dest)
			}

			dirPath := filepath.Join(tmpDir, file.Name())
			childFiles, err := ioutil.ReadDir(dirPath)
			if err != nil {
				return errors.Trace(err)
			}
			for _, cf := range childFiles {
				if cf.IsDir() && cf.Name() == "bin" {
					return os.Rename(dirPath, dest)
				}
			}
		}
	}

	return errors.Errorf("not found bin dir under %s", tmpDir)
}

// FormateArgs return formate args from conf
func FormateArgs(conf map[string]string) []string {
	var args = make([]string, 0, len(conf))
	for arg, value := range conf {
		args = append(args, fmt.Sprintf("--%s=%s", arg, value))
	}

	return args
}

func runesIsLowerBefore(runes []rune, idx int) bool {
	return idx > 0 && unicode.IsLower(runes[idx-1])
}

func runesIsMinusBefore(runes []rune, idx int) bool {
	return idx > 0 && runes[idx-1] == '-'
}

// FormatSnakeCase format snake case variable -  '-' + lower char
func FormatSnakeCase(str string) string {
	str = strings.Replace(strings.TrimSpace(str), " ", "-", -1)
	str = strings.Trim(strings.Replace(str, "_", "-", -1), "-")

	runes := []rune(str)
	length := len(runes)

	var ret []rune
	for i := 0; i < length; i++ {
		if i > 0 && unicode.IsUpper(runes[i]) && runesIsLowerBefore(runes, i) {
			ret = append(ret, '-')
		}

		if runes[i] != '-' || !runesIsMinusBefore(runes, i) {
			ret = append(ret, unicode.ToLower(runes[i]))
		}
	}

	return string(ret)
}

// Clone clones object deep copy (base 'reflect')
func Clone(dst, src interface{}) error {
	var buf bytes.Buffer
	if err := gob.NewEncoder(&buf).Encode(src); err != nil {
		return err
	}
	return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}

// RandString returns a rand string
func RandString(size int) string {
	sz := make([]rune, size)
	cnt := len(alphabet)
	for i := range sz {
		sz[i] = alphabet[rand.Intn(cnt)]
	}
	return string(sz)
}

// RetryOnError defines a action with retry when "fn" returns error,
// we can specify the number and interval of retries
func RetryOnError(ctx context.Context, retryCount int, fn func() error) error {
	var err error
	for i := 0; i < retryCount; i++ {
		select {
		case <-ctx.Done():
			return nil
		default:
		}
		err = fn()
		if err == nil {
			break
		}

		log.Error(err)
		Sleep(ctx, 2*time.Second)
	}

	return errors.Trace(err)
}

// RetryOnError2 defines a action with retry when "fn" returns error,
// we can specify the number and interval of retries
func RetryOnError2(ctx context.Context, retryCount int, retryInterval time.Duration, fn func() (error, bool)) error {
	var (
		err  error
		exit bool
	)

	for i := 0; i < retryCount; i++ {
		select {
		case <-ctx.Done():
			return nil
		default:
		}
		err, exit = fn()
		if err == nil {
			break
		}

		if exit {
			break
		}

		log.Error(err)

		Sleep(ctx, retryInterval)
	}

	return errors.Trace(err)
}

// RetryCheck defines a action with retry when "fn" returns false
func RetryCheck(ctx context.Context, retryCount int, fn func() bool) bool {
	for i := 0; i < retryCount; i++ {
		select {
		case <-ctx.Done():
			return false
		default:
		}
		if fn() {
			return true
		}

		Sleep(ctx, 10*time.Second)
	}

	return false
}

// Sleep defines special `sleep` with context
func Sleep(ctx context.Context, sleepTime time.Duration) {
	ticker := time.NewTicker(sleepTime)
	defer ticker.Stop()

	select {
	case <-ctx.Done():
		return
	case <-ticker.C:
		return
	}
}

// UniqID returns uniq id for storage
func UniqID() int64 {
	id := atomic.AddInt64(&uid, 1)
	index, _ := strconv.ParseInt(fmt.Sprintf("%d%d", time.Now().Unix(), id), 10, 64)
	return index
}

// InitLogger inits logger
func InitLogger(logFile string) error {
	if logFile == "" {
		return nil
	}
	log.SetRotateByDay()
	log.SetHighlighting(false)
	return errors.Trace(log.SetOutputByName(logFile))
}

// IsProbableEOF detects probable eof
func IsProbableEOF(err error) bool {
	switch {
	case err == io.EOF:
		return true
	case err.Error() == "http: can't write HTTP request on broken connection":
		return true
	case strings.Contains(err.Error(), "connection reset by peer"):
		return true
	case strings.Contains(strings.ToLower(err.Error()), "use of closed network connection"):
		return true
	case strings.Contains(err.Error(), "connection is closing"):
		return true
	default:
		return false
	}
}

// GenLabels genrates labels map from str
// eg: str="app=tikv,selector=schrodinger"
func GenLabels(str string) map[string]string {
	labels := make(map[string]string)
	lbsStr := strings.Split(strings.TrimSpace(str), ",")

	for _, label := range lbsStr {
		vals := strings.Split(label, "=")
		if len(vals) != 2 {
			continue
		}

		labels[strings.TrimSpace(vals[0])] = strings.TrimSpace(vals[1])
	}

	return labels
}

// LabelsMapToLabels converts map to label string
// eg: map["key1"] = "value1", map["key2"]= "value2" => "key1=value1, key2=value2"
func LabelsMapToLabels(labels map[string]string) string {
	var labelsArray []string
	for key, value := range labels {
		labelsArray = append(labelsArray, fmt.Sprintf("%s=%s", key, value))
	}
	return strings.Join(labelsArray, ",")
}

// FatalLogAndBelow retrieves the line fatal and lines below fatal
func FatalLogAndBelow(target string) string {
	if !strings.Contains(target, "[fatal]") {
		return ""
	}

	logs := strings.Split(target, "\n")
	index := len(logs) - 1
	for ; index >= 0; index-- {
		if strings.Contains(logs[index], "[fatal]") {
			break
		}
	}

	return strings.Join(logs[index:], "\n")
}

// OpenDB opens db
func OpenDB(dsn string, maxIdeleConns int) (*sql.DB, error) {
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return nil, err
	}

	db.SetMaxIdleConns(maxIdeleConns)
	log.Info("DB opens successfully")
	return db, nil
}

// IsEmpty determine a string is empty
func IsEmpty(s string) bool {
	return len(strings.TrimSpace(s)) == 0
}

func binTarName(name string) string {
	return fmt.Sprintf("%s.tar.gz", path.Base(name))
}

func getVersion(binName string) (string, error) {
	cmdStr := fmt.Sprintf("%s -version", binName)
	version, err := exec.Command("/bin/sh", "-c", cmdStr).CombinedOutput()
	if err != nil {
		return "", errors.Annotatef(err, "failed to get version of :%s cmd: %s", binName, cmdStr)
	}
	return getGitHash(string(version)), nil
}

// getGitHash get gitHash from git commit hash, hashLength is hash length
func getGitHash(ver string) string {
	if len(ver) < hashLength {
		return ""
	}

	// ver[Git Hash: xxxxxxxx]
	idx := strings.LastIndex(ver, "Hash:")
	if idx < 0 {
		return ""
	}

	return ver[idx+6 : idx+hashLength+6]
}

// CopyFile copys file from src to dst
func CopyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return errors.Trace(err)
	}
	defer in.Close()

	out, err := os.Create(dst)
	if err != nil {
		return errors.Trace(err)
	}
	defer func() {
		if e := out.Close(); e != nil {
			log.Errorf("Close erro %s", e)
		}
	}()

	_, err = io.Copy(out, in)
	if err != nil {
		return errors.Trace(err)
	}

	err = out.Sync()
	if err != nil {
		return errors.Trace(err)
	}

	si, err := os.Stat(src)
	if err != nil {
		return errors.Trace(err)
	}
	err = os.Chmod(dst, si.Mode())
	if err != nil {
		return errors.Trace(err)
	}

	return nil
}

// CopyDir recursively copies a directory tree, attempting to preserve permissions.
// Source directory must exist, destination directory must *not* exist.
// Symlinks are ignored and skipped.
func CopyDir(src string, dst string) error {
	src = filepath.Clean(src)
	dst = filepath.Clean(dst)

	si, err := os.Stat(src)
	if err != nil {
		return errors.Trace(err)
	}
	if !si.IsDir() {
		return errors.New("source is not a directory")
	}

	_, err = os.Stat(dst)
	if err != nil && !os.IsNotExist(err) {
		return errors.Trace(err)
	}
	if err == nil {
		return errors.New("destination already exists")
	}

	err = os.MkdirAll(dst, si.Mode())
	if err != nil {
		return errors.Trace(err)
	}

	entries, err := ioutil.ReadDir(src)
	if err != nil {
		return errors.Trace(err)
	}

	for _, entry := range entries {
		srcPath := filepath.Join(src, entry.Name())
		dstPath := filepath.Join(dst, entry.Name())

		if entry.IsDir() {
			err = CopyDir(srcPath, dstPath)
			if err != nil {
				return errors.Trace(err)
			}
		} else {
			// Skip symlinks.
			if entry.Mode()&os.ModeSymlink != 0 {
				continue
			}

			err = CopyFile(srcPath, dstPath)
			if err != nil {
				return errors.Trace(err)
			}
		}
	}

	return nil
}

// ValidateListenAddr validates listen addr
func ValidateListenAddr(addr string) error {
	if strings.Contains(addr, "0.0.0.0") || strings.HasPrefix(addr, ":") {
		return errors.Errorf("invalid addr %s, must specified a public network card", addr)
	}
	return nil
}

// GenExperimentNamespace generates experiment namespace
func GenExperimentNamespace(experimentName string) string {
	return FormatSnakeCase(fmt.Sprintf("%s-tests", experimentName))
}

// GenExperimentLink generates experiments details
func GenExperimentLink(experimentID int64) string {
	return fmt.Sprintf("http://%s/api/experiments/%d/detail", config.GetGlobalConfig().Util.ManagerDomain, experimentID)
}

// GenBenchHistoryLink generates the link of the bench history
func GenBenchHistoryLink(boxID int64) string {
	return fmt.Sprintf("http://%s/#/bench?boxid=%d", config.GetGlobalConfig().Util.ManagerDomain, boxID)
}

// MatchInArray returns true if the given string value is in the array.
func MatchInArray(arr []string, value string) bool {
	for _, v := range arr {
		if v == value {
			return true
		}
	}
	return false
}

// MatchInArray2 returns true if the array is empty or the given string value is in the array.
func MatchInArray2(arr []string, value string) bool {
	if len(arr) == 0 {
		return true
	}

	for _, v := range arr {
		if strings.ToLower(v) == strings.ToLower(value) {
			return true
		}
	}
	return false
}

// CountDiff returns the diff between the two values
func CountDiff(value, lastValue float64) float64 {
	if lastValue == 0 {
		return 0
	}

	return (value - lastValue) / lastValue
}

// Float64ToPercentage formats float64 to percentage
func Float64ToPercentage(val float64) string {
	return fmt.Sprintf("%.2f%%", val*100)
}

// FormatFloat64 format float64
func FormatFloat64(val float64) string {
	return fmt.Sprintf("%.2f", val)
}

// MapCopy returns a copy of the map
func MapCopy(m map[string]string) map[string]string {
	if m == nil {
		return nil
	}

	copy := make(map[string]string, len(m))
	for key, val := range m {
		copy[key] = val
	}

	return copy
}

// WriteToFile writes string to file
func WriteToFile(data string, filename string) error {
	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
	if err != nil {
		return errors.Trace(err)
	}

	_, err = f.WriteString(data)
	if err != nil {
		return errors.Trace(err)
	}

	f.Sync()
	f.Close()
	return nil
}

// GenExperimentName generates experiment name
func GenExperimentName(boxName string, boxIndex int64) string {
	return fmt.Sprintf("%s-exp%d", boxName, boxIndex)
}

// GenCatName generates cat name
func GenCatName(experimentName string, catCount int64) string {
	return fmt.Sprintf("%s-cat%d", experimentName, catCount)
}

// GenRandomBackupName generates a random name of the backup
func GenRandomBackupName() string {
	return fmt.Sprintf("backup-%d", UniqID())
}

// RemoveRep removes duplicate data
func RemoveRep(arr []string) []string {
	var results []string
	mp := make(map[string]string)
	for _, a := range arr {
		if _, ok := mp[a]; !ok {
			results = append(results, a)
		}

		mp[a] = a
	}

	return results
}

// MergeMaps merges two map,
// the values of the m1 may be overwrited by the values of the m2
func MergeMaps(m1, m2 map[string]string) map[string]string {
	if m1 == nil && m2 == nil {
		return make(map[string]string)
	}

	if m1 == nil {
		return m2
	}

	if m2 == nil {
		return m1
	}

	for key, val := range m2 {
		m1[key] = val
	}

	return m1
}

// CheckIsNetworkError returns true if the error is about network
func CheckIsNetworkError(err error) bool {
	if err == nil {
		return false
	}

	if strings.Contains(strings.ToLower(err.Error()), "i/o timeout") {
		return true
	}

	if strings.Contains(strings.ToLower(err.Error()), "connection refused") {
		return true
	}

	return false
}

// GetHashByBranchAndTag gets git commit with branch and tag
func GetHashByBranchAndTag(branch, tag, repo string, addr string) (string, error) {
	if IsEmpty(branch) && IsEmpty(tag) {
		return "", errors.New("branch or tag is required")
	}

	if IsEmpty(repo) {
		return "", errors.New("repo is required")
	}

	flag := strings.TrimSpace(branch)

	if IsEmpty(flag) {
		flag = strings.TrimSpace(tag)
	}

	hashURL := fmt.Sprintf("%s/download/refs/pingcap/%s/%s/sha1",
		addr, repo, flag)
	_, res, err := curl.Get(hashURL)
	if err != nil {
		log.Errorf("failed to achieve hash code of %v from %s", repo, hashURL)
		return "", errors.Trace(err)
	}
	defer res.Body.Close()

	responseData, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Errorf("failed to read response due %v", err)
		return "", errors.Trace(err)
	}

	gitHash := string(responseData)

	return strings.Trim(gitHash, "\n \t"), nil
}

// ExtractRepoInfo extracts repo info.
func ExtractRepoInfo(repoAddress string) (string, string, error) {
	// special case: repoAddress is "tidb" / "pd" / "tikv"
	if !strings.Contains(repoAddress, "github.com") {
		return "", repoAddress, nil
	}

	reg := regexp.MustCompile(`^(http:\/\/|https:\/\/|ssh\@|\w?)github.com/([\w-]+)/([\w-]+)(\.git|\w?)$`)
	subs := reg.FindStringSubmatch(repoAddress)
	if subs == nil || len(subs) != 5 || IsEmpty(subs[2]) || IsEmpty(subs[3]) {
		return "", "", errors.Errorf("invalid git repo address %s", repoAddress)
	}

	return subs[2], subs[3], nil
}
