package db

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

	"github.com/go-gormigrate/gormigrate/v2"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/atomic"
	"go.uber.org/zap"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/plugin/opentelemetry/tracing"

	"github.com/pingcap-inc/clinic_cloud_server/pkg/config"
)

var globalDB *gorm.DB

var initialized atomic.Bool

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

func Initialize(cfg *config.DBConfig, readOnly bool) error {
	if initialized.Swap(true) {
		return errors.New("global database is already initialized")
	}

	if !readOnly {
		if err := CreateDBIfAbsent(cfg); err != nil {
			return err
		}
	}

	db, err := Connect(cfg)
	if err != nil {
		return err
	}
	globalDB = db

	if readOnly {
		log.Info("Initialize global database successfully")
		return nil
	}

	/**
	 * At this time we just use the simplest mechanism gorm provided (https://gorm.io/docs/migration.html)
	 * to migrate. In the future, we should use more formal way to do this. Maybe like following (not work):
	 *
	 *	goMigration := gormigrate.New(globalDB, gormigrate.DefaultOptions, []*gormigrate.Migration{})
	 *	goMigration.InitSchema(initDB)
	 *	if err := goMigration.Migrate(); err != nil {
	 *		return err
	 *	}
	 */
	//if err := initDB(db); err != nil {
	//	return err
	//}
	var gormMigrations []*gormigrate.Migration
	for _, item := range GetMigrationRecords() {
		gormMigrations = append(gormMigrations, &gormigrate.Migration{
			ID:      fmt.Sprintf("%d", item.Version),
			Migrate: item.Migrate,
		})
	}

	goMigration := gormigrate.New(globalDB, gormigrate.DefaultOptions, gormMigrations)
	goMigration.InitSchema(initDB)
	if err := goMigration.Migrate(); err != nil {
		return err
	}
	log.Info("Initialize global database successfully")

	return nil
}

func initDB(db *gorm.DB) error {
	return db.Set("gorm:table_options", "DEFAULT CHARSET=utf8").AutoMigrate(Models...)
}

func Connect(cfg *config.DBConfig) (*gorm.DB, error) {
	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
	}

	gormCfg := &gorm.Config{
		Logger: &loggerAdapter{
			level:                   logLevel,
			enableTrace:             cfg.EnableDetailedLog, // Debug queries, `true` for detailed logs
			ignoreRecordNotFoundErr: true,
		},
		DisableForeignKeyConstraintWhenMigrating: true,
	}

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

	// Instrument with OpenTelemetry
	if err := db.Use(tracing.NewPlugin(tracing.WithoutMetrics())); err != nil {
		return nil, err
	}

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

	if cfg.ConnMaxLifetime == 0 {
		cfg.ConnMaxLifetime = time.Minute * 3
	}
	if cfg.MaxIdleConns == 0 {
		cfg.MaxIdleConns = 50
	}
	if cfg.MaxOpenConns == 0 {
		cfg.MaxOpenConns = 50
	}
	sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
	sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
	sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)

	return db, nil
}

func CreateDBIfAbsent(cfg *config.DBConfig) error {
	db, err := gorm.Open(mysql.Open(cfg.DSNWithTestDB()))
	if err != nil {
		return err
	}

	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 db.Exec("CREATE DATABASE IF NOT EXISTS " + cfg.Database).Error
}

func Transaction(f func(tx *gorm.DB) error, opts ...*sql.TxOptions) (err error) {
	ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
	defer cancel()
	defer func() {
		if r := recover(); r != nil {
			err = errors.Errorf("%v", r)
		}
	}()

	err = globalDB.WithContext(ctx).Transaction(f, opts...)
	return
}

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