package lock

import (
	"context"
	"database/sql"

	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type lockRepoImpl struct {
	dbProvider db.Provider
}

func NewLockRepo(dbProvider db.Provider) repo.LockRepo {
	return &lockRepoImpl{
		dbProvider: dbProvider,
	}
}

func (l *lockRepoImpl) Lock(ctx context.Context, lockEn *entity.Lock) (func() error, error) {
	wdb := l.dbProvider.WriteDB(ctx)

	// MySQL advisory locks (GET_LOCK/RELEASE_LOCK) are connection-specific.
	// When called inside a transaction (server layer), WriteDB returns the tx-bound *gorm.DB
	// which already pins the connection — so GORM queries are safe.
	// When called outside a transaction (cron layer), WriteDB returns the base *gorm.DB
	// backed by a connection pool, and successive Raw() calls may use different connections,
	// causing RELEASE_LOCK to fail with "not held by this connection".
	//
	// To handle both cases, we grab a dedicated *sql.Conn from the underlying *sql.DB.
	// This connection is held until the returned unlock function is called.
	// Advisory locks are independent of SQL transactions, so using a separate connection
	// does not affect the correctness of transaction-based callers.
	sqlDB, err := wdb.DB()
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get sql.DB for lock %s: %v", lockEn.Key, err)
	}

	conn, err := sqlDB.Conn(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get dedicated connection for lock %s: %v", lockEn.Key, err)
	}

	var out sql.NullBool
	if err := conn.QueryRowContext(ctx, `SELECT GET_LOCK(?,?)`, lockEn.Key, lockEn.TimeoutSecs).Scan(&out); err != nil {
		if closeErr := conn.Close(); closeErr != nil {
			tilog.Warn(ctx, "failed to close connection after GET_LOCK error", zap.String("lockKey", lockEn.Key), zap.Error(closeErr))
		}
		return nil, status.Errorf(codes.Internal, "failed to get lock %s: %v", lockEn.Key, err)
	}

	if !out.Valid || !out.Bool {
		if closeErr := conn.Close(); closeErr != nil {
			tilog.Warn(ctx, "failed to close connection after lock not acquired", zap.String("lockKey", lockEn.Key), zap.Error(closeErr))
		}
		return nil, status.Errorf(codes.FailedPrecondition, "failed to get lock %s", lockEn.Key)
	}

	return func() error {
		defer func() {
			if closeErr := conn.Close(); closeErr != nil {
				tilog.Warn(ctx, "failed to close dedicated lock connection", zap.String("lockKey", lockEn.Key), zap.Error(closeErr))
			}
		}()

		var out sql.NullBool
		if err := conn.QueryRowContext(ctx, `SELECT RELEASE_LOCK(?)`, lockEn.Key).Scan(&out); err != nil {
			tilog.Warn(ctx, "failed to release lock", zap.String("lockKey", lockEn.Key), zap.Error(err))
			return status.Errorf(codes.Internal, "failed to release lock %s: %v", lockEn.Key, err)
		}

		tilog.Info(ctx, "release lock result", zap.String("lockKey", lockEn.Key), zap.Bool("valid", out.Valid), zap.Bool("released", out.Bool))

		if !out.Valid || !out.Bool {
			return status.Errorf(codes.FailedPrecondition, "unable to release lock %s, not held by this connection", lockEn.Key)
		}
		return nil
	}, nil
}

func (l *lockRepoImpl) IsUsed(ctx context.Context, lockEn *entity.Lock) (bool, error) {
	var out sql.NullInt64
	rdb := l.dbProvider.ReadDB(ctx)
	if err := rdb.Raw(`SELECT IS_USED_LOCK(?)`, lockEn.Key).Scan(&out).Error; err != nil {
		return false, status.Errorf(codes.Internal, "failed to check if lock is used: %s", err)
	}
	if !out.Valid {
		return false, nil
	}
	return true, nil
}

func (l *lockRepoImpl) IsFree(ctx context.Context, lockEn *entity.Lock) (bool, error) {
	var out sql.NullBool
	rdb := l.dbProvider.ReadDB(ctx)
	if err := rdb.Raw(`SELECT IS_FREE_LOCK(?)`, lockEn.Key).Scan(&out).Error; err != nil {
		tilog.Warn(ctx, "failed to check if lock is free", zap.String("lockKey", lockEn.Key), zap.Error(err))
		return false, status.Errorf(codes.Internal, "failed to check if lock is free: %s", err)
	}
	if !out.Valid {
		return false, nil
	}
	return out.Bool, nil
}
