package bizerr

import (
	"errors"

	"github.com/go-sql-driver/mysql"
	"google.golang.org/grpc/status"
	"gorm.io/gorm"
)

const (
	mysqlErrCodeDuplicatedKey = 1062
)

// WrapGormError converts gorm error to BizError with predefined ErrCodeRecordNotFound and ErrCodeRecordExist.
// If the error is already a BizError, it will return the original error.
func WrapGormError(err error, format string, args ...any) BizError {
	if err == nil {
		return nil
	}

	code := ErrCodeDB

	if errors.Is(err, gorm.ErrRecordNotFound) {
		code = ErrCodeRecordNotFound
	}

	var mysqlErr *mysql.MySQLError
	if errors.As(err, &mysqlErr) && mysqlErr.Number == mysqlErrCodeDuplicatedKey {
		code = ErrCodeRecordExist
	}

	return WrapErrorWithCode(err, code, format, args...)
}

// WrapGrpcError converts grpc client error to BizError with grpc status code and message.
// If the error is already a BizError, it will return the original error.
func WrapGrpcError(err error, format string, args ...any) BizError {
	grpcErr, ok := status.FromError(err)
	if ok {
		be := New(ErrCode(grpcErr.Code()), "%s", grpcErr.Message())
		if format != "" {
			be = be.Chain(format, args...)
		}
		return be
	}

	return WrapErrorWithCode(err, ErrCodeGRPC, format, args...)
}

// WrapError converts grpc client error to BizError with predefined ErrCodeUnknown.
// If the error is already a BizError, it will return the original error.
func WrapError(err error, format string, args ...any) BizError {
	return WrapErrorWithCode(err, ErrCodeUnknown, format, args...)
}

// WrapErrorWithCode converts an error to BizError with any ErrCode input.
// If the error is already a BizError, it will chain the error message and return the original error.
// If the error is std error, it will create a new BizError with the given code and overwrite the error message.
func WrapErrorWithCode(err error, code ErrCode, format string, args ...any) BizError {
	if err == nil {
		return nil
	}

	var be BizError
	if ok := errors.As(err, &be); ok {
		return be.Chain(format, args...)
	}

	// If the error is not a BizError, create a new one.
	be = New(code, format, args...)
	be = be.Chain("%s", err.Error())
	return be
}

func AsCode(err error, code ErrCode) (BizError, bool) {
	var berr BizError
	if ok := errors.As(err, &berr); !ok {
		return nil, false
	}
	// If the error is not a permission error, return it directly.
	if berr.ErrCode() != code {
		return nil, false
	}
	return berr, true
}
