package db

import (
	"fmt"
	golog "log"
	"os"
	"time"

	mysqlconfig "github.com/go-sql-driver/mysql"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/errors"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// globalDB represents the global database connection pool instance and all
// database operation should use db.Transaction interface to retrieve a
// transaction object.
var globalDB, globalReadOnlyDB, globalSetupDB *gorm.DB

type AuthConfig struct {
	Host     string
	Port     int
	User     string
	Password string
	Database string
}

func getDBAuthConfig(isSetupDB bool) *AuthConfig {
	ret := &AuthConfig{
		User:     config.Get().GetString(config.MySQLUser),
		Password: config.Get().GetString(config.MySQLPassword),
		Host:     config.Get().GetString(config.MySQLHost),
		Port:     config.Get().GetInt(config.MYSQLPort),
	}
	if !isSetupDB {
		ret.Database = config.Get().GetString(config.Database)
	}
	return ret
}

func getReadOnlyAuthConfig() *AuthConfig {
	ret := &AuthConfig{
		User:     config.Get().GetString(config.ReadOnlyMySQLUser),
		Password: config.Get().GetString(config.ReadOnlyMySQLPassword),
		Database: config.Get().GetString(config.ReadOnlyMySQLDatabase),
		Host:     config.Get().GetString(config.ReadOnlyMySQLHost),
		Port:     config.Get().GetInt(config.ReadOnlyMYSQLPort),
	}
	return ret
}

func getDSN(authConfig *AuthConfig) string {
	conf := mysqlconfig.NewConfig()
	conf.Net = "tcp"
	conf.User = authConfig.User
	conf.Addr = fmt.Sprintf("%s:%d", authConfig.Host, authConfig.Port)
	conf.Passwd = authConfig.Password
	conf.DBName = authConfig.Database
	conf.ParseTime = true
	conf.Timeout = config.Get().GetDuration(config.MySQLConnectTimeoutSeconds) * time.Second
	conf.ReadTimeout = config.Get().GetDuration(config.MySQLReadTimeoutSeconds) * time.Second
	conf.WriteTimeout = config.Get().GetDuration(config.MySQLWriteTimeoutSeconds) * time.Second
	conf.Params = map[string]string{"charset": "utf8mb4"}
	conf.Collation = "utf8mb4_bin"
	return conf.FormatDSN()
}

var newLogger = logger.New(
	golog.New(os.Stdout, "\r\n", golog.LstdFlags),
	logger.Config{
		SlowThreshold:             200 * time.Millisecond,
		LogLevel:                  logger.Warn,
		IgnoreRecordNotFoundError: true,
		Colorful:                  false,
	},
)

func initializeDB(conf *AuthConfig) (*gorm.DB, error) {
	dsn := getDSN(conf)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		DisableForeignKeyConstraintWhenMigrating: true,
		Logger:                                   newLogger,
		TranslateError:                           true,
	})
	if err != nil {
		return nil, errors.Trace(err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		return nil, errors.Trace(err)
	}
	sqlDB.SetMaxOpenConns(config.Get().GetInt(config.MaxDatabaseConnections))
	sqlDB.SetMaxIdleConns(config.Get().GetInt(config.MaxDatabaseIDELConnections))
	sqlDB.SetConnMaxLifetime(config.Get().GetDuration(config.MaxDatabaseConnectionLifeTimeSeconds) * time.Second)
	return db, nil
}

func GetSetupDB() (*gorm.DB, error) {
	if globalSetupDB != nil {
		return globalSetupDB, nil
	}
	conf := getDBAuthConfig(true)
	db, err := initializeDB(conf)
	if err != nil {
		return nil, err
	}
	globalSetupDB = db
	return db, nil
}

// Initialize initializes the global database connection pool and return error
// if some unexpected errors from the driver and gorm package raised.
func Initialize() (*gorm.DB, error) {
	var err error
	conf := getDBAuthConfig(false)
	db, err := initializeDB(conf)
	if err != nil {
		return nil, err
	}
	globalDB = db
	readOnlyConf := getReadOnlyAuthConfig()
	readOnlyDB, err := initializeDB(readOnlyConf)
	if err != nil {
		return nil, err
	}
	globalReadOnlyDB = readOnlyDB
	log.Info("Initialize database successfully")
	return db, nil
}

func InitializeStressDB() (*gorm.DB, error) {
	// you can set the value for the stress new db, if not set, it will use default
	var host = ""
	var port = ""
	var user = ""
	var pass = ""

	// use default db
	if host == "" && user == "" {
		user = config.Get().GetString(config.MySQLUser)
		pass = config.Get().GetString(config.MySQLPassword)
		host = config.Get().GetString(config.MySQLHost)
		port = config.Get().GetString(config.MYSQLPort)
	}

	config.Get().Set(config.MySQLUser, user)
	config.Get().Set(config.MySQLPassword, pass)
	config.Get().Set(config.MySQLHost, host)
	config.Get().Set(config.MYSQLPort, port)

	config.Get().Set(config.ReadOnlyMySQLUser, user)
	config.Get().Set(config.ReadOnlyMySQLPassword, pass)
	config.Get().Set(config.ReadOnlyMySQLHost, host)
	config.Get().Set(config.ReadOnlyMYSQLPort, port)

	return Initialize()
}

func GetDB() *gorm.DB {
	return globalDB
}

func GetReadOnlyDB() *gorm.DB {
	return globalReadOnlyDB.Set("gorm:query_option", "SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
}

func CloseAll() error {
	var errs []error

	if globalDB != nil {
		sqlDB, err := globalDB.DB()
		if err == nil {
			errs = append(errs, sqlDB.Close())
		}
		globalDB = nil
	}

	if globalReadOnlyDB != nil {
		sqlDB, err := globalReadOnlyDB.DB()
		if err == nil {
			errs = append(errs, sqlDB.Close())
		}
		globalReadOnlyDB = nil
	}

	if globalSetupDB != nil {
		sqlDB, err := globalSetupDB.DB()
		if err == nil {
			errs = append(errs, sqlDB.Close())
		}
		globalSetupDB = nil
	}

	for _, err := range errs {
		if err != nil {
			return err
		}
	}
	return nil
}
