package db

import (
	"context"
	"database/sql"
	"strings"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/debug_portal/config"
	"github.com/tidbcloud/debug_portal/util"
	"go.uber.org/atomic"
	"go.uber.org/zap"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

const DefaultDBOperationTimeout = 30 * time.Second

var globalDB *gorm.DB

// initialized prevents the database connection pool being initialized multiple
// times. An error will be returned to the caller if the database has been initialized.
var initialized atomic.Bool

// for test only.
func ResetInitialize() {
	initialized.Swap(false)
}

// InitializeWithoutMigrate initializes the global database connection pool and return error
// if some unexpected errors from the driver and gorm package raised.
func InitializeWithoutMigrate(cfg config.MysqlConfig) (*gorm.DB, error) {
	if initialized.Swap(true) {
		return nil, errors.New("global database is already initialized")
	}

	connCfg := config.ConnConfig{
		User:              cfg.User,
		Host:              cfg.Host,
		Database:          cfg.Database,
		Password:          cfg.Password,
		Port:              cfg.Port,
		EnableDetailedLog: false,
		LogLevel:          "info",
	}
	if err := EnsureConnectionDatabase(connCfg); err != nil {
		return nil, errors.Trace(err)
	}
	db, err := Connect(connCfg)
	if err != nil {
		return nil, errors.Trace(err)
	}
	globalDB = db

	log.Info("Initialize global database successfully")

	return globalDB, nil
}

// globalDB represents the global database connection pool instance and all
// database operation should use db.Transaction interface to retrieve a
// transaction object.

// Transaction starts a transaction as a block, return error will rollback, otherwise to commit.
func Transaction(fc func(tx *gorm.DB) error, opts ...*sql.TxOptions) (err error) {
	ctx, cancel := context.WithTimeout(context.TODO(), DefaultDBOperationTimeout)
	defer cancel()
	defer func() {
		if r := recover(); r != nil {
			err = errors.Errorf("%v", r)
		}
	}()
	err = globalDB.WithContext(ctx).Transaction(fc, opts...)
	return
}

// Connect connects to the database and registers validation callbacks
func Connect(cfg config.ConnConfig) (*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: &util.LoggerAdapter{
			Level:       logLevel,
			EnableTrace: cfg.EnableDetailedLog, // Debug queries, `true` for detailed logs
		},
	}

	db, err := gorm.Open(mysql.Open(cfg.DSN()), 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.ConnConfig) error {
	db, err := gorm.Open(mysql.Open(cfg.DSNWithTestDB()), nil)
	if err != nil {
		return errors.Annotate(err, cfg.DSNWithoutPassword())
	}

	defer func() {
		sqlDB, err := db.DB()
		if err != nil {
			log.Error("failed to get sqlDB from db.DB()", zap.Error(err))
			return
		}
		if err := sqlDB.Close(); err != nil {
			log.Error("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
	})
}
