package helper

import (
	"errors"
	"fmt"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type statusWrapper struct {
	error
	status *status.Status
}

func (sw statusWrapper) GRPCStatus() *status.Status {
	return sw.status
}

func (sw statusWrapper) Unwrap() error {
	return sw.error
}

// ErrInvalidArgumentf wraps a formatted error with codes.InvalidArgument, unless the formatted
// error is a wrapped gRPC error.
func ErrInvalidArgumentf(format string, a ...interface{}) error {
	return formatError(codes.InvalidArgument, format, a...)
}

// ErrNotFoundf wraps a formatted error with codes.NotFound, unless the
// formatted error is a wrapped gRPC error.
func ErrNotFoundf(format string, a ...interface{}) error {
	return formatError(codes.NotFound, format, a...)
}

// grpcErrorMessageWrapper is used to wrap a gRPC `status.Status`-style error such that it behaves
// like a `status.Status`, except that it generates a readable error message.
type grpcErrorMessageWrapper struct {
	*status.Status
}

func (e grpcErrorMessageWrapper) GRPCStatus() *status.Status {
	return e.Status
}

func (e grpcErrorMessageWrapper) Error() string {
	return e.Message()
}

func (e grpcErrorMessageWrapper) Unwrap() error {
	return e.Status.Err()
}

type grpcStatuser interface {
	GRPCStatus() *status.Status
}

// formatError will create a new error from the given format string. If the error string contains a
// %w verb and its corresponding error has a gRPC error code, then the returned error will keep this
// gRPC error code instead of using the one provided as an argument.
func formatError(code codes.Code, format string, a ...interface{}) error {
	args := make([]interface{}, 0, len(a))
	for _, a := range a {
		err, ok := a.(error)
		if !ok {
			args = append(args, a)
			continue
		}

		status, ok := status.FromError(err)
		if !ok {
			args = append(args, a)
			continue
		}

		// Wrap gRPC status errors so that the resulting error message stays readable.
		args = append(args, grpcErrorMessageWrapper{status})
	}

	err := fmt.Errorf(format, args...)

	for current := err; current != nil; current = errors.Unwrap(current) {
		nestedCode := GrpcCode(current)
		if nestedCode != codes.OK && nestedCode != codes.Unknown {
			code = nestedCode
		}
	}

	st := status.New(code, err.Error())

	var wrappedGRPCStatus grpcStatuser
	if errors.As(err, &wrappedGRPCStatus) {
		wrappedProto := wrappedGRPCStatus.GRPCStatus().Proto()

		if len(wrappedProto.Details) > 0 {
			proto := st.Proto()
			proto.Details = wrappedProto.Details
			st = status.FromProto(proto)
		}
	}

	return statusWrapper{err, st}
}

// GrpcCode translates errors into codes.Code values.
// It unwraps the nested errors until it finds the most nested one that returns the codes.Code.
// If err is nil it returns codes.OK.
// If no codes.Code found it returns codes.Unknown.
func GrpcCode(err error) codes.Code {
	if err == nil {
		return codes.OK
	}

	code := codes.Unknown
	for ; err != nil; err = errors.Unwrap(err) {
		st, ok := status.FromError(err)
		if ok {
			code = st.Code()
		}
	}

	return code
}
