package db

import (
	"context"
	"sync"

	"gorm.io/gorm"
)

type (
	providerImp    struct{}
	providerCtxKey string
)

const (
	enableNestedTransaction = false
)

var (
	provider         *providerImp
	once             sync.Once
	transactionDBKey providerCtxKey = "dbProviderTransactionDB"
)

// ReadDB return a db handler for read operation
func (p *providerImp) ReadDB(ctx context.Context) *gorm.DB {
	transDB := p.getTransDBFromContext(ctx)
	if transDB != nil {
		return transDB
	}
	return p.newReadDB(ctx)
}

// WriteDB return a db handler for read or write operation
func (p *providerImp) WriteDB(ctx context.Context) *gorm.DB {
	transDB := p.getTransDBFromContext(ctx)
	if transDB != nil {
		return transDB
	}
	return p.newWriteDB(ctx)
}

// Transaction start a DB transaction
func (p *providerImp) Transaction(ctx context.Context, cb func(ctx context.Context) error) error {
	transDB := p.getTransDBFromContext(ctx)
	if transDB == nil {
		wdb := p.newWriteDB(ctx)
		return wdb.Transaction(func(tx *gorm.DB) error {
			newCtx := p.withTransDB(ctx, tx)
			return cb(newCtx)
		})
	}

	// enable transaction
	if enableNestedTransaction {
		return transDB.Transaction(func(tx *gorm.DB) error {
			newCtx := p.withTransDB(ctx, tx)
			return cb(newCtx)
		})
	}

	// disable nested transaction
	// call callback directly.
	// We can acquire the TRANSACTIONAL db handler from context within callback
	return cb(ctx)
}

func (p *providerImp) newReadDB(ctx context.Context) *gorm.DB {
	return globalDB.WithContext(ctx)
}

func (p *providerImp) newWriteDB(ctx context.Context) *gorm.DB {
	return globalDB.WithContext(ctx)
}

func (p *providerImp) getTransDBFromContext(ctx context.Context) *gorm.DB {
	db, ok := ctx.Value(transactionDBKey).(*gorm.DB)
	if !ok {
		return nil
	}
	return db
}

func (p *providerImp) withTransDB(ctx context.Context, db *gorm.DB) context.Context {
	newCtx := context.WithValue(ctx, transactionDBKey, db)
	return newCtx
}
