package db

import (
	"context"
	"database/sql"
	"runtime/debug"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/atomic"
	"gorm.io/gorm"

	"github.com/tidbcloud/dataflow-service/internal/config"
	"github.com/tidbcloud/pkgv2/gormutil"
)

type Provider interface {
	ReadDB(ctx context.Context) *gorm.DB
	WriteDB(ctx context.Context) *gorm.DB
}

// Transactioner provide the ability to start a transaction
type Transactioner interface {
	Transaction(ctx context.Context, cb func(ctx context.Context) error) error
}

// TransProvider provide DB handle and the transaction
type TransProvider interface {
	Provider
	Transactioner
}

// globalDB represents the global database connection pool instance and all
// database operation should use db.Transaction interface to retrieve a
// transaction object.
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 gormutil.Config) (*gorm.DB, error) {
	if initialized.Swap(true) {
		return nil, errors.New("global database is already initialized")
	}
	cfg.EnableDetailedLog = config.Global().ServerConfig.EnableDBQueryLog
	cfg.LogLevel = config.Global().ServerConfig.LogLevel
	if err := gormutil.EnsureConnectionDatabase(cfg); err != nil {
		return nil, errors.Trace(err)
	}
	db, err := gormutil.Connect(cfg)
	if err != nil {
		return nil, errors.Trace(err)
	}
	globalDB = db

	log.Info(context.TODO(), "Initialize global database successfully")

	return globalDB, nil
}

// GetDBWithContext create a new db session and set ctx to the new instantiated db
func GetDBWithContext(ctx context.Context) *gorm.DB {
	return globalDB.WithContext(ctx)
}

// GetDB create a new db session
func GetDB() *gorm.DB {
	return globalDB.Session(&gorm.Session{})
}

// TransactionWithContext starts a transaction as a block, return error will rollback, otherwise to commit.
// The transaction is executed in a specific context
func TransactionWithContext(ctx context.Context, fc func(tx *gorm.DB) error, opts ...*sql.TxOptions) (err error) {
	if globalDB == nil {
		return errors.New("global database is not initialized")
	}
	txCtx, cancel := context.WithTimeout(ctx, config.DefaultDBOperationTimeout)
	defer cancel()
	defer func() {
		if r := recover(); r != nil {
			err = errors.Errorf("%v", r)
			log.Error(ctx, "db.transaction error stack:"+string(debug.Stack()))
		}
	}()
	err = globalDB.WithContext(txCtx).Transaction(fc, opts...)
	return
}

// 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) error {
	return TransactionWithContext(context.TODO(), fc, opts...)
}

// Create inserts a new record into the database.
func Create(obj interface{}) error {
	return Transaction(func(tx *gorm.DB) error { return tx.Create(obj).Error })
}

func Delete(value interface{}, conds ...interface{}) error {
	return Transaction(func(tx *gorm.DB) error {
		return tx.Delete(value, conds...).Error
	})
}
