package main

import (
	"archive/tar"
	"compress/gzip"
	"database/sql"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type StatusDumper struct {
	db       DBInfo
	sdInfo   *SDInfo
	baseDir  string
	sqlDir   string
	statsDir string
}

func (sd *StatusDumper) sqlPath() string {
	return filepath.Join(sd.baseDir, sd.sqlDir, "schema.sql")
}

func (sd *StatusDumper) statsPath(outfile string) string {
	return filepath.Join(sd.baseDir, sd.sqlDir, sd.statsDir, outfile)
}

func (sd *StatusDumper) Init(db DBInfo, sdInfo *SDInfo) error {
	sd.db = db
	sd.sdInfo = sdInfo
	dir := strings.TrimSpace(*out)
	if dir == "" {
		dir = "."
	}
	bdir, err := filepath.Abs(dir)
	if err != nil {
		return fmt.Errorf("StatusDumper init err:%s", err.Error())
	}
	sd.baseDir = bdir
	sd.sqlDir = fmt.Sprintf("stats_dump_%s", time.Now().Format("20060102T150405"))
	sd.statsDir = "stats"
	err = os.MkdirAll(filepath.Join(bdir, sd.sqlDir, sd.statsDir), 0766)
	if err != nil {
		return fmt.Errorf("StatusDumper init dir err:%s", err.Error())
	}
	return nil
}

func (sd *StatusDumper) StatsDump() {
	dbwr, err := os.Create(sd.sqlPath())
	if err != nil {
		LogFatal("open '%s/schema.sql' err:%s.", sd.sqlDir, err.Error())
	}
	defer dbwr.Close()
	sd.tidbVersion(dbwr)
	for _, db := range sd.sdInfo.Schemas {
		dbname := db.Schema
		obj := newMysql(sd.db)
		sd.dumpDB(dbname, obj, dbwr)
		for _, tbl := range db.Tables {
			sd.dumpTable(dbname, tbl.Name, obj, dbwr)
		}
	}
}

func (sd *StatusDumper) dumpDB(db string, obj *sql.DB, wr io.WriteCloser) {
	LogInfo("dumpDB %s", db)
	sv, err := getShowCreate(obj, fmt.Sprintf("show create database %s", db))
	if err != nil {
		LogFatal("dumpDB(%s).getShowCreate err:%s", db, err.Error())
	}
	fmt.Fprintf(wr, "-- Database %s schema\n", db)
	fmt.Fprintf(wr, "%s;\nuse %s;\n\n", sv.SQL, db)
}

func (sd *StatusDumper) dumpTable(db, tbl string, obj *sql.DB, dbwr io.WriteCloser) {
	dbTbl := fmt.Sprintf("%s.%s", db, tbl)
	LogInfo("dumpTable %s", dbTbl)

	// dump schema;
	sv, err := getShowCreate(obj, fmt.Sprintf("show create table %s", dbTbl))
	if err != nil {
		LogFatal("dumpDB(%s).getShowCreate err:%s", dbTbl, err.Error())
	}
	fmt.Fprintf(dbwr, "-- Table %s schema \n", dbTbl)
	fmt.Fprintf(dbwr, "%s;\n", sv.SQL)
	//
	addr := sd.db.TidbStat
	url := makeStatsUrl(addr, db, tbl)
	outfile := fmt.Sprintf("%s.json", dbTbl)
	dbwr.Write([]byte(fmt.Sprintf("LOAD STATS '%s'; \n\n", filepath.Join(sd.statsDir, outfile))))
	// dump stats;
	d, err := getStatusDump(addr, db, tbl)
	if err != nil {
		LogErr("dumpTableStats(%s).getStatusDump err:%s.\n\tYou can execute cmd 'curl -sl %s > %s' .", dbTbl, err.Error(), url, outfile)
		return
	}
	fp, err := os.Create(sd.statsPath(outfile))
	if err != nil {
		LogErr("dumpTableStats(%s) err:%s.\n\tYou can execute cmd 'curl -sl %s > %s' .", dbTbl, err.Error(), url, outfile)
		return
	}
	fp.Write(d)
	fp.Close()
}

func (sd *StatusDumper) tidbVersion(wr io.WriteCloser) {
	obj := newMysql(sd.db)
	str, err := getTidbVersion(obj)
	if err != nil {
		LogErr("tidbVersion err:%s", err.Error())
	}
	fmt.Fprintf(wr, "-- TiDB Version (%s:%s) \n", sd.db.Host, sd.db.Port)
	fmt.Fprintf(wr, "/*\n%s\n*/ \n\n", str)
}

func (sd *StatusDumper) Finish() {
	src := filepath.Join(sd.baseDir, sd.sqlDir)
	dst := filepath.Join(sd.baseDir, fmt.Sprintf("%s.tar.gz", sd.sqlDir))
	//
	fw, err := os.Create(dst)
	if err != nil {
		LogErr("StatusDumper.Finish.Create(%s) err:%s", sd.sqlDir, err.Error())
		return
	}
	defer fw.Close()
	//
	gw := gzip.NewWriter(fw)
	defer gw.Close()
	//
	tw := tar.NewWriter(gw)
	defer tw.Close()
	// do process;
	err = filepath.Walk(src, func(fileName string, fi os.FileInfo, err error) error {
		// 因为这个闭包会返回个 error ，所以先要处理一下这个
		if err != nil {
			return err
		}

		// 这里就不需要我们自己再 os.Stat 了，它已经做好了，我们直接使用 fi 即可
		hdr, err := tar.FileInfoHeader(fi, "")
		if err != nil {
			return err
		}
		// 这里需要处理下 hdr 中的 Name，因为默认文件的名字是不带路径的，
		// 打包之后所有文件就会堆在一起，这样就破坏了原本的目录结果
		// 例如： 将原本 hdr.Name 的 syslog 替换程 log/syslog
		// 这个其实也很简单，回调函数的 fileName 字段给我们返回来的就是完整路径的 log/syslog
		// strings.TrimPrefix 将 fileName 的最左侧的 / 去掉，
		// 熟悉 Linux 的都知道为什么要去掉这个
		hdr.Name = strings.TrimPrefix(fileName, string(filepath.Separator))

		// 写入文件信息
		if err := tw.WriteHeader(hdr); err != nil {
			return err
		}

		// 判断下文件是否是标准文件，如果不是就不处理了，
		// 如： 目录，这里就只记录了文件信息，不会执行下面的 copy
		if !fi.Mode().IsRegular() {
			return nil
		}

		// 打开文件
		fr, err := os.Open(fileName)
		defer fr.Close()
		if err != nil {
			return err
		}

		// copy 文件数据到 tw
		n, err := io.Copy(tw, fr)
		if err != nil {
			return err
		}

		// 记录下过程，这个可以不记录，这个看需要，这样可以看到打包的过程
		LogInfo(" tar: %s (%d)", fileName, n)

		return nil
	})
	if err != nil {
		LogErr("StatusDumper.Finish.tar err: %s", err.Error())
	}
}
