package main

import (
	"bufio"
	"bytes"
	"context"
	"database/sql"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/juju/errors"
	client "github.com/pingcap/schrodinger/pkg/test_client"
	"github.com/pingcap/schrodinger/util"
)

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

func getCheckFileName(index int, mode string) SQLCheckFile {
	if mode == "random" {
		count := len(sqlCheckSet)
		index = rand.Intn(count)
	}
	return sqlCheckSet[index]
}

func getSQLFile(ctx context.Context, rawURL string, fileName string) ([]string, error) {
	tmpDir, err := ioutil.TempDir("", "paniccheck")
	if err != nil {
		return nil, err
	}

	defer os.RemoveAll(tmpDir)

	client.Infof("wget %s .....", rawURL)
	var name string
	if name, err = util.Wget(ctx, rawURL, tmpDir); err != nil {
		client.Errorf("download file %s failed %v", rawURL, err)
		return nil, err
	}

	client.Infof("decompression %s .....", rawURL)

	err = exec.CommandContext(ctx, "tar", "-xf", name, "-C", tmpDir).Run()

	if err != nil {
		return nil, errors.Annotate(err, "decompression error")
	}

	fullPath := tmpDir + "/" + fileName
	client.Infof("full path %s", fullPath)
	return loadSQL(fullPath)
}

func loadSQL(fileName string) ([]string, error) {
	f, err := os.Open(fileName)
	if err != nil {
		return nil, err
	}

	client.Info("readline.....")
	buf := bufio.NewReader(f)
	var result []string
	for {
		line, err := buf.ReadString('\n')
		line = strings.TrimSpace(line)
		if err != nil {
			if err == io.EOF {
				return result, nil
			}
			return nil, err
		}
		result = append(result, line)
	}
}

func executePanicCheck(ctx context.Context, cli *client.Client, p *Pos, mode string) (bool, string, error) {
	sqlFileInfo := getCheckFileName(p.FileIndex, mode)
	client.Debugf("panic file %+v", sqlFileInfo)
	sqlLines, err := getSQLFile(ctx, sqlFileInfo.URL, sqlFileInfo.FileName)
	if err != nil {
		client.Errorf("get sqlline failed %v", err)
		return false, "", errors.Errorf("get sqlline failed %v", err)
	}

	err = initReportedPanic(panicReportedURL)
	if err != nil {
		client.Errorf("init report painc failed %v", err)
		return false, "", errors.Trace(err)
	}

	maxLineCount := len(sqlLines)
	client.Info("lines:", maxLineCount)

	dbAddr := cli.TiDBService()
	dbDSN := fmt.Sprintf("root:@tcp(%s)/%s", dbAddr, *dbName)
	db, err := util.OpenDB(dbDSN, 1)
	if err != nil {
		return false, "", errors.Trace(err)
	}

	if db == nil {
		return false, "", nil
	}

	defer db.Close()
	stop := make(chan struct{}, 1)
	done := make(chan struct{}, threadCount)
	for i := 0; i < threadCount; i++ {
		go executeSQL(&sqlLines, maxLineCount, stop, done, db, p, mode)
	}

	checkTicker := time.NewTicker(1 * time.Minute) //Cycle check
	timeOutTicker := time.NewTimer(keepTime + 3*time.Minute)

	defer func() {
		checkTicker.Stop()
		timeOutTicker.Stop()
	}()

	//wait all threads exit
	threadExitCount := 0
	for {
		select {
		case <-done:
			threadExitCount++
			client.Info("thread exit......")
			if threadExitCount >= threadCount {
				close(stop)
				time.Sleep(3 * time.Minute)
				haveNewPanic, data := checkTidbPanicLog(cli)

				if haveNewPanic {
					return true, data, nil
				}
				return false, "", nil
			}
		case <-checkTicker.C:
			client.Info("start check tidb panic log")
			haveNewPanic, data := checkTidbPanicLog(cli)
			if haveNewPanic {
				close(stop)
				return true, data, nil
			}
		case <-timeOutTicker.C:
			close(stop)
			client.Info("\ntime out:", threadCount, time.Now().String())
			return false, "", nil
		}
	}
}

func executeSQL(lines *[]string, maxLineCount int, stop chan struct{}, done chan struct{}, db *sql.DB, p *Pos, mode string) {
	defer func() {
		done <- struct{}{}
	}()
	for p.LineIndex < maxLineCount {
		var index int
		if mode == "random" {
			p.LineIndex = 0
			index = rand.Intn(maxLineCount)
		} else {
			index = p.LineIndex
		}
		select {
		case <-stop:
			client.Info("chan exit")
			return
		default:
			client.Debugf("exec %s", (*lines)[index])
			p.LineIndex++
			_, err := db.Exec((*lines)[index])
			if err != nil {
				_, err := db.Exec(tidbConnCheck)
				if err != nil {
					return
				}
				client.Debugf("*")
			} else {
				client.Debugf(".")
			}
		}
	}
	p.FileIndex++
	p.LineIndex = 0
}

func checkTidbPanicLog(c *client.Client) (bool, string) {
	newPanicCount, data := panicCheck(c)
	return newPanicCount > 0, data
}

func panicCheck(c *client.Client) (int64, string) {
	var (
		totalPaincCount    int64
		totalPaincNewCount int64
		panicBuf           bytes.Buffer
	)
	panicBuf.WriteString("checktime:" + time.Now().String() + "\n")

	total, logs, err := c.FilterTiDBLog("connection running loop panic", "now-15m", "now", 0, 10000)
	if err != nil {
		client.Errorf("failed to check tidb log, error: %v", errors.ErrorStack(err))
		return 0, ""
	}

	if total == 0 {
		client.Info("no panic log in tidb logs")
		return 0, ""
	}

	if total > 10000 {
		client.Errorf("panic log is too much and some panic logs may be lost, you need to reduce time or use other methods")
	}

	client.Infof("get panic log total: %d", total)

	for _, log := range logs {
		client.Infof("fetch panic log: %s", log)
		panics, count, newCount, err := checkTidbLog(log)
		if err == nil {
			panicBuf.WriteString(panics.String())
			totalPaincCount += count
			totalPaincNewCount += newCount
		} else {
			client.Errorf("check tidb log failed %v", errors.ErrorStack(err))
		}
	}

	client.Infof(panicBuf.String())

	return totalPaincNewCount, panicBuf.String()
}

func initReportedPanic(fileURL string) error {
	resp, err := http.Get(fileURL)
	if err != nil {
		return errors.Trace(err)
	}
	defer resp.Body.Close()
	client.Info("Get:", fileURL)

	text, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return errors.Trace(err)
	}

	stacks, err := parsePanicData(text, "\n--\n")
	if err != nil {
		return errors.Trace(err)
	}

	leagel = make(map[string]*PanicStack)

	for _, s := range stacks {
		sig := s.Signature()
		leagel[string(sig)] = s
	}

	return nil
}

func checkTidbLog(log string) (buf bytes.Buffer, panicCount, newPanicCount int64, err error) {
	texts, err := parsePanicData([]byte(log), "connection running loop panic")
	if err != nil {
		return buf, panicCount, newPanicCount, errors.New("pareData err:" + err.Error())
	}

	return checkPanic(texts)
}

func checkPanic(stacks []*PanicStack) (buf bytes.Buffer, panicCount, newPanicCount int64, err error) {
	for _, s := range stacks {
		panicCount++
		client.Debugf("check stack %+v", s.Func)
		sig := s.Signature()
		if _, ok := leagel[string(sig)]; !ok {
			newPanicCount++
			buf.WriteString("\n ---******---\n")
			buf.Write(s.Raw)
			buf.WriteString("\n\n")
		}
	}

	return buf, panicCount, newPanicCount, nil
}

func splitPanicText(input []byte, splitString string) [][]byte {
	return bytes.Split(input, []byte(splitString))
}
