package twirp

import "fmt"

// NewError is the generic constructor for a twirp.Error.
// The ErrorCode must be one of the valid predefined constants,
// otherwise it will be converted to an error {type: Internal, msg: "invalid error type {{code}}"}.
// If you need to add metadata, use .WithMeta(key, value) method after building the error.
func NewError(code ErrorCode, msg string) Error {
	if IsValidErrorCode(code) {
		return &twerr{
			code: code,
			msg:  msg,
		}
	}
	return &twerr{
		code: Internal,
		msg:  "invalid error type " + string(code),
	}
}

// NotFoundError constructor for the common NotFound error.
func NotFoundError(msg string) Error {
	return NewError(NotFound, msg)
}

// InvalidArgumentError constructor for the common InvalidArgument error.
// Can be used when an argument has invalid format, is a number out of range, is a bad option, etc.
// i.e. twirp.InvalidArgumentError("email", "must be a valid email")
//      twirp.InvalidArgumentError("limit", "can not be negative")
// For non-zero validations, use twirp.RequiredArgumentError instead.
func InvalidArgumentError(argument string, validationMsg string) Error {
	err := NewError(InvalidArgument, argument+" "+validationMsg)
	err = err.WithMeta("argument", argument)
	return err
}

// RequiredArgumentError is a more scpecific constructor for InvalidArgument error.
// Should be used when the argument is required (expected to have a non-zero value).
func RequiredArgumentError(argument string) Error {
	return InvalidArgumentError(argument, "is required")
}

// InternalError constructor for the common Internal error.
// Should be used to specify that something bad or unexpected happened.
func InternalError(msg string) Error {
	return NewError(Internal, msg)
}

// InternalErrorWith is an easy way to wrap another error. It adds the error
// implementation type string as Meta("cause"), which can be useful for
// debugging. Should be used in the common case of an unexpected error returned
// from another API, but sometimes is better to build a more specific error
// (i.e. NewError(Unknown, err.Error())).
//
// The returned error also has a Cause() method which will return the original
// error, if it is known. This can be used with the github.com/pkg/errors
// package to extract the root cause of an error. Information about the root
// cause of an error is lost when it is serialized, so this doesn't let a client
// know the exact root cause of a server's error.
func InternalErrorWith(err error) Error {
	msg := err.Error()
	twerr := NewError(Internal, msg)
	twerr = twerr.WithMeta("cause", fmt.Sprintf("%T", err)) // to easily tell apart wrapped internal errors from explicit ones
	return &wrappedErr{
		wrapper: twerr,
		cause:   err,
	}
}

// ServerHTTPStatusFromErrorCode maps a Twirp error type into a similar HTTP response status.
// It is used by the Twirp server handler to set the HTTP response status code.
// Returns 0 if the ErrorCode is invalid.
// Note: This mapping is consistent with the grpc-gateway project: https://github.com/grpc-ecosystem/grpc-gateway/blob/master/runtime/errors.go,
//       except that it returns status 0 for un-identified ErrorCode instead of 500.
func ServerHTTPStatusFromErrorCode(code ErrorCode) int {
	switch code {
	case Canceled:
		return 408 // RequestTimeout
	case Unknown:
		return 500 // Internal Server Error
	case InvalidArgument:
		return 400 // BadRequest
	case DeadlineExceeded:
		return 408 // RequestTimeout
	case NotFound:
		return 404 // Not Found
	case BadRoute:
		return 404 // Not Found
	case AlreadyExists:
		return 409 // Conflict
	case PermissionDenied:
		return 403 // Forbidden
	case Unauthenticated:
		return 401 // Unauthorized
	case ResourceExhausted:
		return 403 // Forbidden
	case FailedPrecondition:
		return 412 // Precondition Failed
	case Aborted:
		return 409 // Conflict
	case OutOfRange:
		return 400 // Bad Request
	case Unimplemented:
		return 501 // Not Implemented
	case Internal:
		return 500 // Internal Server Error
	case Unavailable:
		return 503 // Service Unavailable
	case DataLoss:
		return 500 // Internal Server Error
	case NoError:
		return 200 // OK
	default:
		return 0 // Invalid!
	}
}

// IsValidErrorCode returns true if is one of the valid predefined constants.
func IsValidErrorCode(code ErrorCode) bool {
	return ServerHTTPStatusFromErrorCode(code) != 0
}

// twirp.Error implementation
type twerr struct {
	code ErrorCode
	msg  string
	meta map[string]string
}

func (e *twerr) Code() ErrorCode { return e.code }
func (e *twerr) Msg() string     { return e.msg }

func (e *twerr) Meta(key string) string {
	if e.meta != nil {
		return e.meta[key] // also returns "" if key is not in meta map
	}
	return ""
}

func (e *twerr) WithMeta(key string, value string) Error {
	newErr := &twerr{
		code: e.code,
		msg:  e.msg,
		meta: make(map[string]string, len(e.meta)),
	}
	for k, v := range e.meta {
		newErr.meta[k] = v
	}
	newErr.meta[key] = value
	return newErr
}

func (e *twerr) MetaMap() map[string]string {
	return e.meta
}

func (e *twerr) Error() string {
	return fmt.Sprintf("twirp error %s: %s", e.code, e.msg)
}

// wrappedErr fulfills the twirp.Error interface and the
// github.com/pkg/errors.Causer interface. It exposes all the twirp error
// methods, but root cause of an error can be retrieved with
// (*wrappedErr).Cause. This is expected to be used with the InternalErrorWith
// function.
type wrappedErr struct {
	wrapper Error
	cause   error
}

func (e *wrappedErr) Code() ErrorCode            { return e.wrapper.Code() }
func (e *wrappedErr) Msg() string                { return e.wrapper.Msg() }
func (e *wrappedErr) Meta(key string) string     { return e.wrapper.Meta(key) }
func (e *wrappedErr) MetaMap() map[string]string { return e.wrapper.MetaMap() }
func (e *wrappedErr) Error() string              { return e.wrapper.Error() }
func (e *wrappedErr) WithMeta(key string, val string) Error {
	return &wrappedErr{
		wrapper: e.wrapper.WithMeta(key, val),
		cause:   e.cause,
	}
}
func (e *wrappedErr) Cause() error { return e.cause }
