package db

import (
	"context"
	"fmt"
	"runtime"
	"sync/atomic"
	"time"

	"github.com/go-gormigrate/gormigrate/v2"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/uptrace/opentelemetry-go-extra/otelgorm"
	"go.uber.org/zap"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/plugin/prometheus"
)

var (
	globalDB    *gorm.DB
	initialized atomic.Bool
)

func NewProvider(gormDB *gorm.DB) db.TransProvider {
	// only for test
	if gormDB == nil {
		return db.NewProvider(globalDB)
	}

	provider := db.NewProvider(gormDB)
	return provider
}

// Open opens the global schema. Not thread-safe.
func InitDB(cfg *Config, migrates []*gormigrate.Migration) *gorm.DB {
	if initialized.Swap(true) {
		return globalDB
	}

	// If migrates is not empty, create the database if it does not exist.
	if len(migrates) > 0 {
		gormdb, err := gorm.Open(mysql.Open(cfg.DSNWithInitDB()))
		defer func() {
			sqlDB, _ := gormdb.DB()
			_ = sqlDB.Close()
		}()
		if err != nil {
			panic(err)
		}
		if err := createDBIfAbsent(gormdb, cfg); err != nil {
			panic(err)
		}
	}

	gormdb, err := initDriver(cfg)
	if err != nil {
		panic(err)
	}

	if err := migrate(gormdb, migrates); err != nil {
		panic(err)
	}

	tilog.Info(
		context.Background(),
		"global database is successfully opened",
		zap.String("configYaml: \n", cfg.LogYamlWithoutPwd()),
		zap.String("dsn", cfg.LogDSNWithoutPwd()),
	)
	globalDB = gormdb
	return gormdb
}

func initDriver(cfg *Config) (*gorm.DB, error) {
	gormCfg := &gorm.Config{
		DisableForeignKeyConstraintWhenMigrating: true,
	}

	gormdb, err := gorm.Open(mysql.Open(cfg.DSN()), gormCfg)
	if err != nil {
		return nil, err
	}

	sqlDB, err := gormdb.DB()
	if err != nil {
		return nil, err
	}

	if cfg.ConnMaxLifetime == 0 {
		cfg.ConnMaxLifetime = time.Minute * 3
	}
	if cfg.MaxIdleConnections == 0 {
		cfg.MaxIdleConnections = runtime.NumCPU()
	}
	if cfg.MaxOpenConnections == 0 {
		cfg.MaxOpenConnections = runtime.NumCPU() * 10
	}
	sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
	sqlDB.SetMaxIdleConns(cfg.MaxIdleConnections)
	sqlDB.SetMaxOpenConns(cfg.MaxOpenConnections)

	// Add metric and tracing plugins
	if err := gormdb.Use(otelgorm.NewPlugin(
		otelgorm.WithoutMetrics(),
		otelgorm.WithDBName(cfg.Database),
	)); err != nil {
		return nil, err
	}

	if err := gormdb.Use(prometheus.New(prometheus.Config{
		DBName:          cfg.Database,
		RefreshInterval: 15,
		StartServer:     false,
	})); err != nil {
		panic(err)
	}

	return gormdb, nil
}

func createDBIfAbsent(gormdb *gorm.DB, cfg *Config) error {
	defer func() {
		sqlDB, err := gormdb.DB()
		if err != nil {
			tilog.Error(context.Background(), "failed to get sqlDB from db.DB()", zap.Error(err))
			return
		}

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

	return gormdb.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", cfg.Database)).Error
}

func migrate(gormdb *gorm.DB, migrates []*gormigrate.Migration) error {
	goMigration := gormigrate.New(gormdb, gormigrate.DefaultOptions, migrates)
	return goMigration.Migrate()
}
