package main

import (
	"database/sql"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"net/http"
	_ "net/http/pprof"

	"github.com/juju/errors"
	"github.com/pingcap/schrodinger-test/util"
	client "github.com/pingcap/schrodinger/pkg/test_client"
	"golang.org/x/net/context"
)

var (
	binlogVersionTag         string
	mysqlVersion             string
	run_duration             time.Duration
	disturb_pump_duration    time.Duration
	disturb_drainer_duration time.Duration
	kill_pump                bool
	selectedCases            testcaseNames
)

func init() {
	flag.StringVar(&binlogVersionTag, "binlog-version-tag", "cluster-latest", "use https://download.pingcap.org/tidb-binlog-{{binlog-version-tag}}-linux-amd64.tar.gz to download binlog binary")
	flag.StringVar(&mysqlVersion, "mysql-version", "5.7", "mysql version")
	flag.DurationVar(&run_duration, "run-duration", time.Hour, "run how long before quit")
	flag.DurationVar(&disturb_pump_duration, "disturb-pump-duration", 0, "disturb pump duration")
	flag.DurationVar(&disturb_drainer_duration, "disturb-drainer-duration", 0, "disturb drainer duration")
	flag.BoolVar(&kill_pump, "kill-pump", false, "whether use SIGKILL (kill -9) for pump")
	flag.Var(&selectedCases, "cases", "comma-separated names of cases to cover")
}

func main() {
	flag.Parse()
	client.Info("[binlog] start...")

	// Select genData and bank by default to keep backward compatible
	if len(selectedCases) == 0 {
		selectedCases = []string{"genData", "bank"}
	}
	client.Infof("Test cases selected: %+v", selectedCases)

	ctx, cancel := context.WithCancel(context.Background())
	cli := client.New(ctx, client.ModeOnline)

	go startHttpServer()

	sc := make(chan os.Signal, 1)
	signal.Notify(sc,
		syscall.SIGHUP,
		syscall.SIGINT,
		syscall.SIGTERM,
		syscall.SIGQUIT)
	go func() {
		sig := <-sc
		client.Infof("[binlog] Got signal [%s] to exist.", sig)
		cancel()
		os.Exit(0)
	}()

	pumps, err := setUpPump(cli, binlogVersionTag)
	if err != nil {
		client.Fatalf("[binlog] return with error %v", client.ErrorStack(err))
	}
	client.Info("set up pumps success")

	mysql, err := cli.CreateMysql(mysqlVersion, 0)
	if err != nil {
		client.Fatalf("[binlog] create mysql error %v", client.ErrorStack(err))
	}
	client.Info("set up mysql success")

	// create test database on mysql
	// well, mysql may not be available after `cli.CreateMysql` return nil
	// so we try for some time, fatal if still fail
	tryStart := time.Now()
	for {
		err := createTestDB(mysql.Host)
		if err == nil {
			break
		}

		if time.Since(tryStart) > time.Minute {
			client.Fatalf("[binlog] Fail to create test table: %s", err)
		}
		time.Sleep(time.Second)
	}

	drainer, err := setUpDrainer(cli, binlogVersionTag, mysql.Host)
	if err != nil {
		client.Fatalf("[binlog] setup drainer error %v", err)
	}
	client.Info("set up drainer success")

	dbAddr := cli.TiDBService()
	dbHost := strings.Split(dbAddr, ":")[0]
	dbDSN := fmt.Sprintf("root:@tcp(%s)/test", dbAddr)
	db, err := util.OpenDB(dbDSN, 200)
	if err != nil {
		client.Fatalf("[binlog] create db client error %v", err)
	}

	testcases := collectCases(db, selectedCases)

	nemesis := nemesis{
		cli:                      cli,
		alivePumps:               pumps,
		drainer:                  drainer,
		disturb_pump_duration:    disturb_pump_duration,
		disturb_drainer_duration: disturb_drainer_duration,
		kill_pump:                kill_pump,
	}

	go func() {
		nemesis.resume()
		err := nemesis.do()
		if err != nil {
			client.Fatalf("[binlog] nemesis error %v", err)
		}
	}()

	for _, c := range testcases {
		go c.run(db)
	}

	startTime := time.Now()
	checkTicker := time.NewTicker(5 * time.Minute)
	for {
		select {
		case <-checkTicker.C:
			client.Debug("start check..")

			for _, c := range testcases {
				c.pause()
			}
			nemesis.pause()
			client.Debug("has pause nemesis")

			err := nemesis.startPumps()
			if err != nil {
				client.Fatalf("[binlog] nemesis error %v", err)
			}
			client.Debug("has start pumps")

			err = nemesis.startDrainer()
			if err != nil {
				client.Fatalf("[binlog] nemesis error %v", err)
			}
			client.Debug("has start drainer")

			tryNum := 60
			var idx int
			for idx = 0; idx < tryNum; idx++ {
				client.Infof("check data try num: %d", idx)

				err = checkData(dbHost, mysql.Host)
				if err != nil {
					client.Infof("check data fails %v", err)
					time.Sleep(60 * time.Second)
					continue
				}

				break
			}

			if idx == tryNum {
				client.Fatalf("checkData fail after %d try", tryNum)
			} else {
				client.Infof("pass check data")
			}

			for _, c := range testcases {
				c.resume()
			}
			nemesis.resume()

			if time.Since(startTime) > run_duration {
				return
			}
		}
	}
}

func startHttpServer() {
	port := 9000

	for p := port; p < port+1000; p++ {
		client.Info("http on port: ", p)
		err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%v", p), nil)
		if err != nil {
			client.Error(err)
		}
	}
}

func createTestDB(host string) error {
	// create test database on mysql
	mysqlDB, err := createDB("root", "", host, 3306, "")
	if err != nil {
		return errors.Trace(err)
	}
	_, err = mysqlDB.Query("create database if not exists test;")
	if err != nil {
		return errors.Trace(err)
	}
	mysqlDB.Close()

	return nil
}

func collectCases(db *sql.DB, caseNames testcaseNames) []testcase {
	selected := make(map[string]bool, len(caseNames))
	for _, name := range caseNames {
		selected[name] = true
	}
	cases := make([]testcase, 0, len(caseNames))
	if _, ok := selected["bank"]; ok {
		bank, err := createBank(db)
		if err != nil {
			client.Fatalf("[binlog] Fail to create bank: %v", err)
		}
		cases = append(cases, bank)
	}

	if _, ok := selected["genData"]; ok {
		genData := createGenData(db)
		cases = append(cases, genData)
	}
	return cases
}
