package helper

import (
	"bufio"
	"database/sql"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/backend/global"
	_ "github.com/go-sql-driver/mysql"
)

func init() {}

type dumpOption struct {
	isData bool

	tables      []string
	isAllTable  bool
	isDropTable bool
	writer      io.Writer
}

type DumpOption func(*dumpOption)

func WithDropTable() DumpOption {
	return func(option *dumpOption) {
		option.isDropTable = true
	}
}

func WithData() DumpOption {
	return func(option *dumpOption) {
		option.isData = true
	}
}

func WithTables(tables ...string) DumpOption {
	return func(option *dumpOption) {
		option.tables = tables
	}
}

func WithAllTable() DumpOption {
	return func(option *dumpOption) {
		option.isAllTable = true
	}
}

func WithWriter(writer io.Writer) DumpOption {
	return func(option *dumpOption) {
		option.writer = writer
	}
}

func Dump(dns string, opts ...DumpOption) error {
	start := time.Now()
	global.LOG.Infof("dump start at %s\n", start.Format("2006-01-02 15:04:05"))
	defer func() {
		end := time.Now()
		global.LOG.Infof("dump end at %s, cost %s\n", end.Format("2006-01-02 15:04:05"), end.Sub(start))
	}()

	var err error

	var o dumpOption

	for _, opt := range opts {
		opt(&o)
	}

	if len(o.tables) == 0 {
		o.isAllTable = true
	}

	if o.writer == nil {
		o.writer = os.Stdout
	}

	buf := bufio.NewWriter(o.writer)
	defer buf.Flush()

	itemFile, lineNumber := "", 0

	itemFile += "-- ----------------------------\n"
	itemFile += "-- MySQL Database Dump\n"
	itemFile += "-- Start Time: " + start.Format("2006-01-02 15:04:05") + "\n"
	itemFile += "-- ----------------------------\n\n\n"

	db, err := sql.Open("mysql", dns)
	if err != nil {
		global.LOG.Errorf("open mysql db failed, err: %v", err)
		return err
	}
	defer db.Close()

	dbName, err := getDBNameFromDNS(dns)
	if err != nil {
		global.LOG.Errorf("get db name from dns failed, err: %v", err)
		return err
	}
	_, err = db.Exec(fmt.Sprintf("USE `%s`", dbName))
	if err != nil {
		global.LOG.Errorf("exec `use %s` failed, err: %v", dbName, err)
		return err
	}

	var tables []string
	if o.isAllTable {
		tmp, err := getAllTables(db)
		if err != nil {
			global.LOG.Errorf("get all tables failed, err: %v", err)
			return err
		}
		tables = tmp
	} else {
		tables = o.tables
	}

	for _, table := range tables {
		if o.isDropTable {
			itemFile += fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table)
		}

		itemFile += "-- ----------------------------\n"
		itemFile += fmt.Sprintf("-- Table structure for %s\n", table)
		itemFile += "-- ----------------------------\n"

		createTableSQL, err := getCreateTableSQL(db, table)
		if err != nil {
			global.LOG.Errorf("get create table sql failed, err: %v", err)
			return err
		}
		itemFile += createTableSQL
		itemFile += ";\n\n\n\n"

		if o.isData {
			itemFile += "-- ----------------------------\n"
			itemFile += fmt.Sprintf("-- Records of %s\n", table)
			itemFile += "-- ----------------------------\n"

			lineRows, err := db.Query(fmt.Sprintf("SELECT * FROM `%s`", table))
			if err != nil {
				global.LOG.Errorf("exec `select * from %s` failed, err: %v", table, err)
				return err
			}
			defer lineRows.Close()

			var columns []string
			columns, err = lineRows.Columns()
			if err != nil {
				global.LOG.Errorf("get columes falied, err: %v", err)
				return err
			}
			columnTypes, err := lineRows.ColumnTypes()
			if err != nil {
				global.LOG.Errorf("get colume types failed, err: %v", err)
				return err
			}
			for lineRows.Next() {
				row := make([]interface{}, len(columns))
				rowPointers := make([]interface{}, len(columns))
				for i := range columns {
					rowPointers[i] = &row[i]
				}
				if err = lineRows.Scan(rowPointers...); err != nil {
					global.LOG.Errorf("scan row data failed, err: %v", err)
					return err
				}
				ssql := loadDataSql(row, columnTypes, table)
				if len(ssql) != 0 {
					itemFile += ssql
					lineNumber++
				}
				if lineNumber > 500 {
					_, _ = buf.WriteString(itemFile)
					itemFile = ""
					lineNumber = 0
				}
			}

			itemFile += "\n\n"
		}
	}

	itemFile += "-- ----------------------------\n"
	itemFile += "-- Dumped by mysqldump\n"
	itemFile += "-- Cost Time: " + time.Since(start).String() + "\n"
	itemFile += "-- ----------------------------\n"

	_, _ = buf.WriteString(itemFile)
	_ = buf.Flush()

	return nil
}

func getCreateTableSQL(db *sql.DB, table string) (string, error) {
	var createTableSQL string
	err := db.QueryRow(fmt.Sprintf("SHOW CREATE TABLE `%s`", table)).Scan(&table, &createTableSQL)
	if err != nil {
		return "", err
	}
	createTableSQL = strings.Replace(createTableSQL, "CREATE TABLE", "CREATE TABLE IF NOT EXISTS", 1)
	return createTableSQL, nil
}

func getAllTables(db *sql.DB) ([]string, error) {
	var tables []string
	rows, err := db.Query("SHOW TABLES")
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var table string
		err = rows.Scan(&table)
		if err != nil {
			return nil, err
		}
		tables = append(tables, table)
	}
	return tables, nil
}

func loadDataSql(row []interface{}, columnTypes []*sql.ColumnType, table string) string {
	ssql := "INSERT INTO `" + table + "` VALUES ("
	for i, col := range row {
		if col == nil {
			ssql += "NULL"
		} else {
			Type := columnTypes[i].DatabaseTypeName()
			Type = strings.Replace(Type, "UNSIGNED", "", -1)
			Type = strings.Replace(Type, " ", "", -1)
			switch Type {
			case "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "INTEGER", "BIGINT":
				if bs, ok := col.([]byte); ok {
					ssql += string(bs)
				} else {
					ssql += fmt.Sprintf("%d", col)
				}
			case "FLOAT", "DOUBLE":
				if bs, ok := col.([]byte); ok {
					ssql += string(bs)
				} else {
					ssql += fmt.Sprintf("%f", col)
				}
			case "DECIMAL", "DEC":
				ssql += fmt.Sprintf("%s", col)

			case "DATE":
				t, ok := col.(time.Time)
				if !ok {
					global.LOG.Errorf("the DATE type conversion failed, err value: %v", col)
					return ""
				}
				ssql += fmt.Sprintf("'%s'", t.Format("2006-01-02"))
			case "DATETIME":
				t, ok := col.(time.Time)
				if !ok {
					global.LOG.Errorf("the DATETIME type conversion failed, err value: %v", col)
					return ""
				}
				ssql += fmt.Sprintf("'%s'", t.Format("2006-01-02 15:04:05"))
			case "TIMESTAMP":
				t, ok := col.(time.Time)
				if !ok {
					global.LOG.Errorf("the TIMESTAMP type conversion failed, err value: %v", col)
					return ""
				}
				ssql += fmt.Sprintf("'%s'", t.Format("2006-01-02 15:04:05"))
			case "TIME":
				t, ok := col.([]byte)
				if !ok {
					global.LOG.Errorf("the TIME type conversion failed, err value: %v", col)
					return ""
				}
				ssql += fmt.Sprintf("'%s'", string(t))
			case "YEAR":
				t, ok := col.([]byte)
				if !ok {
					global.LOG.Errorf("the YEAR type conversion failed, err value: %v", col)
					return ""
				}
				ssql += string(t)
			case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT":
				ssql += fmt.Sprintf("'%s'", strings.Replace(fmt.Sprintf("%s", col), "'", "''", -1))
			case "BIT", "BINARY", "VARBINARY", "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB":
				ssql += fmt.Sprintf("0x%X", col)
			case "ENUM", "SET":
				ssql += fmt.Sprintf("'%s'", col)
			case "BOOL", "BOOLEAN":
				if col.(bool) {
					ssql += "true"
				} else {
					ssql += "false"
				}
			case "JSON":
				ssql += fmt.Sprintf("'%s'", col)
			default:
				global.LOG.Errorf("unsupported colume type: %s", Type)
				return ""
			}
		}
		if i < len(row)-1 {
			ssql += ","
		}
	}
	ssql += ");\n"
	return ssql
}
