// Package gormutil adds utilities to our gorm setup.
// It is designed to be imported in place of gorm.
// nolint
package gormutil

import (
	"context"
	"strings"
	"time"

	"github.com/tidbcloud/account/commons/config"

	mysqlDriver "github.com/go-sql-driver/mysql"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
)

// Connect connects to the database and registers validation callbacks
func Connect(cfg Config) (*gorm.DB, error) {
	// We cannot map all log levels of zap into gorm logger.LogLevel.
	var logLevel logger.LogLevel
	switch strings.ToLower(cfg.LogLevel) {
	case "fatal":
		logLevel = logger.Silent
	case "error":
		logLevel = logger.Error
	case "warn":
		logLevel = logger.Warn
	default:
		logLevel = logger.Info
	}

	gormConfig := &gorm.Config{
		Logger: &loggerAdapter{

			level:       logLevel,
			enableTrace: cfg.EnableDetailedLog, // Debug queries, `true` for detailed logs
		},
		NamingStrategy: schema.NamingStrategy{
			NameReplacer: strings.NewReplacer("OpenAPI", "openapi"),
		},
		DisableNestedTransaction: cfg.DisableNestedTransaction,
	}

	mysqlStr := cfg.DSN()
	// if config.IsLocalEnv() {
	// 	mysqlStr = mysqlStr + "&tls=true"
	// }
	if config.IsDevEnv() || config.IsLocalEnv() {
		// print sql log
		gormConfig.Logger = logger.Default.LogMode(logger.Info)
	}

	db, err := gorm.Open(mysql.Open(mysqlStr), gormConfig)
	if err != nil {
		return nil, errors.Annotate(err, cfg.DSNWithoutPassword())
	}

	sqlDB, err := db.DB()
	if err != nil {
		return nil, errors.Annotate(err, cfg.DSNWithoutPassword())
	}
	if cfg.ConnMaxLifetime == 0 {
		cfg.ConnMaxLifetime = time.Minute * 3
	}
	if cfg.MaxIdleConns == 0 {
		cfg.MaxIdleConns = 100
	}
	if cfg.MaxOpenConns == 0 {
		cfg.MaxOpenConns = 100
	}
	sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
	sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
	sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)

	// Register struct validator
	// validations.RegisterCallbacks(db)

	return db, nil
}

// EnsureConnectionDatabase ensures the database is created
func EnsureConnectionDatabase(cfg Config) error {
	mysqlStr := cfg.DSNWithTestDB()
	// if config.IsLocalEnv() {
	//	mysqlStr += "&tls=true"
	// }
	db, err := gorm.Open(mysql.Open(mysqlStr), nil)
	if err != nil {
		return errors.Annotate(err, cfg.DSNWithoutPassword())
	}

	defer func() {
		sqlDB, err := db.DB()
		ctx := context.Background()
		if err != nil {
			tilog.Error(ctx, "failed to get sqlDB from db.DB()", zap.Error(err))
			return
		}
		if err := sqlDB.Close(); err != nil {
			tilog.Error(ctx, "failed to close db.DB()", zap.Error(err))
			return
		}
	}()

	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Exec("CREATE DATABASE IF NOT EXISTS " + cfg.Database).Error
	})
}

// Escape implements SQL escaping. The driver does not accept some placeholders
func Escape(sql string) string {
	dest := make([]byte, 0, 2*len(sql))
	var escape byte
	for i := 0; i < len(sql); i++ {
		c := sql[i]

		escape = 0

		switch c {
		case 0: /* Must be escaped for 'mysql' */
			escape = '0'
		case '\n': /* Must be escaped for logs */
			escape = 'n'
		case '\r':
			escape = 'r'
		case '\\':
			escape = '\\'
		case '\'':
			escape = '\''
		case '"': /* Better safe than sorry */
			escape = '"'
		case '\032': /* This gives problems on Win32 */
			escape = 'Z'
		}

		if escape != 0 {
			dest = append(dest, '\\', escape)
		} else {
			dest = append(dest, c)
		}
	}

	return string(dest)
}

func IsDuplicatedError(err error) bool {
	switch err.(type) {
	case *mysqlDriver.MySQLError:
		return err.(*mysqlDriver.MySQLError).Number == 1062
	}
	return false
}
