// Copyright 2021-2026 The Connect Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package connect

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
)

const (
	commonErrorsURL          = "https://connectrpc.com/docs/go/common-errors"
	defaultAnyResolverPrefix = "type.googleapis.com/"
)

var (
	// errNotModified signals Connect-protocol responses to GET requests to use the
	// 304 Not Modified HTTP error code.
	errNotModified = errors.New("not modified")
	// errNotModifiedClient wraps ErrNotModified for use client-side.
	errNotModifiedClient = fmt.Errorf("HTTP 304: %w", errNotModified)
)

// An ErrorDetail is a self-describing Protobuf message attached to an [*Error].
// Error details are sent over the network to clients, which can then work with
// strongly-typed data rather than trying to parse a complex error message. For
// example, you might use details to send a localized error message or retry
// parameters to the client.
//
// The [google.golang.org/genproto/googleapis/rpc/errdetails] package contains a
// variety of Protobuf messages commonly used as error details.
type ErrorDetail struct {
	pbAny    *anypb.Any
	pbInner  proto.Message // if nil, must be extracted from pbAny
	wireJSON string        // preserve human-readable JSON
}

// NewErrorDetail constructs a new error detail. If msg is an *[anypb.Any] then
// it is used as is. Otherwise, it is first marshalled into an *[anypb.Any]
// value. This returns an error if msg cannot be marshalled.
func NewErrorDetail(msg proto.Message) (*ErrorDetail, error) {
	// If it's already an Any, don't wrap it inside another.
	if pb, ok := msg.(*anypb.Any); ok {
		return &ErrorDetail{pbAny: pb}, nil
	}
	pb, err := anypb.New(msg)
	if err != nil {
		return nil, err
	}
	return &ErrorDetail{pbAny: pb, pbInner: msg}, nil
}

// Type is the fully-qualified name of the detail's Protobuf message (for
// example, acme.foo.v1.FooDetail).
func (d *ErrorDetail) Type() string {
	// proto.Any tries to make messages self-describing by using type URLs rather
	// than plain type names, but there aren't any descriptor registries
	// deployed. With the current state of the `Any` code, it's not possible to
	// build a useful type registry either. To hide this from users, we should
	// trim the URL prefix is added to the type name.
	//
	// If we ever want to support remote registries, we can add an explicit
	// `TypeURL` method.
	return typeNameForURL(d.pbAny.GetTypeUrl())
}

// Bytes returns a copy of the Protobuf-serialized detail.
func (d *ErrorDetail) Bytes() []byte {
	out := make([]byte, len(d.pbAny.GetValue()))
	copy(out, d.pbAny.GetValue())
	return out
}

// Value uses the Protobuf runtime's package-global registry to unmarshal the
// Detail into a strongly-typed message. Typically, clients use Go type
// assertions to cast from the proto.Message interface to concrete types.
func (d *ErrorDetail) Value() (proto.Message, error) {
	if d.pbInner != nil {
		// We clone it so that if the caller mutates the returned value,
		// they don't inadvertently corrupt this error detail value.
		return proto.Clone(d.pbInner), nil
	}
	return d.pbAny.UnmarshalNew()
}

// An Error captures four key pieces of information: a [Code], an underlying Go
// error, a map of metadata, and an optional collection of arbitrary Protobuf
// messages called "details" (more on those below). Servers send the code, the
// underlying error's Error() output, the metadata, and details over the wire
// to clients. Remember that the underlying error's message will be sent to
// clients - take care not to leak sensitive information from public APIs!
//
// Service implementations and interceptors should return errors that can be
// cast to an [*Error] (using the standard library's [errors.As]). If the returned
// error can't be cast to an [*Error], connect will use [CodeUnknown] and the
// returned error's message.
//
// Error details are an optional mechanism for servers, interceptors, and
// proxies to attach arbitrary Protobuf messages to the error code and message.
// They're a clearer and more performant alternative to HTTP header
// microformats. See [the documentation on errors] for more details.
//
// [the documentation on errors]: https://connectrpc.com/docs/go/errors
type Error struct {
	code    Code
	err     error
	details []*ErrorDetail
	meta    http.Header
	wireErr bool
}

// NewError annotates any Go error with a status code.
func NewError(c Code, underlying error) *Error {
	return &Error{code: c, err: underlying}
}

// NewWireError is similar to [NewError], but the resulting *Error returns true
// when tested with [IsWireError].
//
// This is useful for clients trying to propagate partial failures from
// streaming RPCs. Often, these RPCs include error information in their
// response messages (for example, [gRPC server reflection] and
// OpenTelemetry's [OTLP]). Clients propagating these errors up the stack
// should use NewWireError to clarify that the error code, message, and details
// (if any) were explicitly sent by the server rather than inferred from a
// lower-level networking error or timeout.
//
// [gRPC server reflection]: https://github.com/grpc/grpc/blob/v1.49.2/src/proto/grpc/reflection/v1alpha/reflection.proto#L132-L136
// [OTLP]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#partial-success
func NewWireError(c Code, underlying error) *Error {
	err := NewError(c, underlying)
	err.wireErr = true
	return err
}

// IsWireError checks whether the error was returned by the server, as opposed
// to being synthesized by the client.
//
// Clients may find this useful when deciding how to propagate errors. For
// example, an RPC-to-HTTP proxy might expose a server-sent CodeUnknown as an
// HTTP 500 but a client-synthesized CodeUnknown as a 503.
//
// Handlers will strip [Error.Meta] headers propagated from wire errors to avoid
// leaking response headers. To propagate headers recreate the error as a
// non-wire error.
func IsWireError(err error) bool {
	se := new(Error)
	if !errors.As(err, &se) {
		return false
	}
	return se.wireErr
}

// NewNotModifiedError indicates that the requested resource hasn't changed. It
// should be used only when handlers wish to respond to conditional HTTP GET
// requests with a 304 Not Modified. In all other circumstances, including all
// RPCs using the gRPC or gRPC-Web protocols, it's equivalent to sending an
// error with [CodeUnknown]. The supplied headers should include Etag,
// Cache-Control, or any other headers required by [RFC 9110 § 15.4.5].
//
// Clients should check for this error using [IsNotModifiedError].
//
// [RFC 9110 § 15.4.5]: https://httpwg.org/specs/rfc9110.html#status.304
func NewNotModifiedError(headers http.Header) *Error {
	err := NewError(CodeUnknown, errNotModified)
	if headers != nil {
		err.meta = headers
	}
	return err
}

func (e *Error) Error() string {
	message := e.Message()
	if message == "" {
		return e.code.String()
	}
	return e.code.String() + ": " + message
}

// Message returns the underlying error message. It may be empty if the
// original error was created with a status code and a nil error.
func (e *Error) Message() string {
	if e.err != nil {
		return e.err.Error()
	}
	return ""
}

// Unwrap allows [errors.Is] and [errors.As] access to the underlying error.
func (e *Error) Unwrap() error {
	return e.err
}

// Code returns the error's status code.
func (e *Error) Code() Code {
	return e.code
}

// Details returns the error's details.
func (e *Error) Details() []*ErrorDetail {
	return e.details
}

// AddDetail appends to the error's details.
func (e *Error) AddDetail(d *ErrorDetail) {
	e.details = append(e.details, d)
}

// Meta allows the error to carry additional information as key-value pairs.
//
// Protocol-specific headers and trailers may be removed to avoid breaking
// protocol semantics. For example, Content-Length and Content-Type headers
// won't be propagated. See the documentation for each protocol for more
// datails.
//
// When clients receive errors, the metadata contains the union of the HTTP
// headers and the protocol-specific trailers (either HTTP trailers or in-body
// metadata).
func (e *Error) Meta() http.Header {
	if e.meta == nil {
		e.meta = make(http.Header)
	}
	return e.meta
}

func (e *Error) detailsAsAny() []*anypb.Any {
	anys := make([]*anypb.Any, 0, len(e.details))
	for _, detail := range e.details {
		anys = append(anys, detail.pbAny)
	}
	return anys
}

// IsNotModifiedError checks whether the supplied error indicates that the
// requested resource hasn't changed. It only returns true if the server used
// [NewNotModifiedError] in response to a Connect-protocol RPC made with an
// HTTP GET.
func IsNotModifiedError(err error) bool {
	return errors.Is(err, errNotModified)
}

// errorf calls fmt.Errorf with the supplied template and arguments, then wraps
// the resulting error.
func errorf(c Code, template string, args ...any) *Error {
	return NewError(c, fmt.Errorf(template, args...))
}

// asError uses errors.As to unwrap any error and look for a connect *Error.
func asError(err error) (*Error, bool) {
	var connectErr *Error
	ok := errors.As(err, &connectErr)
	return connectErr, ok
}

// wrapIfUncoded ensures that all errors are wrapped. It leaves already-wrapped
// errors unchanged, uses wrapIfContextError to apply codes to context.Canceled
// and context.DeadlineExceeded, and falls back to wrapping other errors with
// CodeUnknown.
func wrapIfUncoded(err error) error {
	if err == nil {
		return nil
	}
	maybeCodedErr := wrapIfContextError(err)
	if _, ok := asError(maybeCodedErr); ok {
		return maybeCodedErr
	}
	return NewError(CodeUnknown, maybeCodedErr)
}

// wrapIfContextError applies CodeCanceled or CodeDeadlineExceeded to Go's
// context.Canceled and context.DeadlineExceeded errors, but only if they
// haven't already been wrapped.
func wrapIfContextError(err error) error {
	if err == nil {
		return nil
	}
	if _, ok := asError(err); ok {
		return err
	}
	if errors.Is(err, context.Canceled) {
		return NewError(CodeCanceled, err)
	}
	if errors.Is(err, context.DeadlineExceeded) {
		return NewError(CodeDeadlineExceeded, err)
	}
	// Ick, some dial errors can be returned as os.ErrDeadlineExceeded
	// instead of context.DeadlineExceeded :(
	// https://github.com/golang/go/issues/64449
	if errors.Is(err, os.ErrDeadlineExceeded) {
		return NewError(CodeDeadlineExceeded, err)
	}
	return err
}

// wrapIfContextDone wraps errors with CodeCanceled or CodeDeadlineExceeded
// if the context is done. It leaves already-wrapped errors unchanged.
func wrapIfContextDone(ctx context.Context, err error) error {
	if err == nil {
		return nil
	}
	err = wrapIfContextError(err)
	if _, ok := asError(err); ok {
		return err
	}
	ctxErr := ctx.Err()
	if errors.Is(ctxErr, context.Canceled) {
		return NewError(CodeCanceled, err)
	} else if errors.Is(ctxErr, context.DeadlineExceeded) {
		return NewError(CodeDeadlineExceeded, err)
	}
	return err
}

// wrapIfLikelyH2CNotConfiguredError adds a wrapping error that has a message
// telling the caller that they likely need to use h2c but are using a raw http.Client{}.
//
// This happens when running a gRPC-only server.
// This is fragile and may break over time, and this should be considered a best-effort.
func wrapIfLikelyH2CNotConfiguredError(request *http.Request, err error) error {
	if err == nil {
		return nil
	}
	if _, ok := asError(err); ok {
		return err
	}
	if url := request.URL; url != nil && url.Scheme != "http" {
		// If the scheme is not http, we definitely do not have an h2c error, so just return.
		return err
	}
	// net/http code has been investigated and there is no typing of any of these errors
	// they are all created with fmt.Errorf
	// grpc-go returns the first error 2/3-3/4 of the time, and the second error 1/4-1/3 of the time
	if errString := err.Error(); strings.HasPrefix(errString, `Post "`) &&
		(strings.Contains(errString, `net/http: HTTP/1.x transport connection broken: malformed HTTP response`) ||
			strings.HasSuffix(errString, `write: broken pipe`)) {
		return fmt.Errorf("possible h2c configuration issue when talking to gRPC server, see %s: %w", commonErrorsURL, err)
	}
	return err
}

// wrapIfLikelyWithGRPCNotUsedError adds a wrapping error that has a message
// telling the caller that they likely forgot to use connect.WithGRPC().
//
// This happens when running a gRPC-only server.
// This is fragile and may break over time, and this should be considered a best-effort.
func wrapIfLikelyWithGRPCNotUsedError(err error) error {
	if err == nil {
		return nil
	}
	if _, ok := asError(err); ok {
		return err
	}
	// golang.org/x/net code has been investigated and there is no typing of this error
	// it is created with fmt.Errorf
	// http2/transport.go:573:	return nil, fmt.Errorf("http2: Transport: cannot retry err [%v] after Request.Body was written; define Request.GetBody to avoid this error", err)
	if errString := err.Error(); strings.HasPrefix(errString, `Post "`) &&
		strings.Contains(errString, `http2: Transport: cannot retry err`) &&
		strings.HasSuffix(errString, `after Request.Body was written; define Request.GetBody to avoid this error`) {
		return fmt.Errorf("possible missing connect.WithGPRC() client option when talking to gRPC server, see %s: %w", commonErrorsURL, err)
	}
	return err
}

// HTTP/2 has its own set of error codes, which it sends in RST_STREAM frames.
// When the server sends one of these errors, we should map it back into our
// RPC error codes following
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#http2-transport-mapping.
//
// This would be vastly simpler if we were using x/net/http2 directly, since
// the StreamError type is exported. When x/net/http2 gets vendored into
// net/http, though, all these types become unexported...so we're left with
// string munging.
func wrapIfRSTError(ctx context.Context, err error) error {
	const (
		streamErrPrefix = "stream error: "
		fromPeerSuffix  = "; received from peer"
	)
	if err == nil {
		return nil
	}
	if _, ok := asError(err); ok {
		return err
	}
	if urlErr := new(url.Error); errors.As(err, &urlErr) {
		// If we get an RST_STREAM error from http.Client.Do, it's wrapped in a
		// *url.Error.
		err = urlErr.Unwrap()
	}
	msg := err.Error()
	if !strings.HasPrefix(msg, streamErrPrefix) {
		return err
	}
	if !strings.HasSuffix(msg, fromPeerSuffix) {
		return err
	}
	msg = strings.TrimSuffix(msg, fromPeerSuffix)
	i := strings.LastIndex(msg, ";")
	if i < 0 || i >= len(msg)-1 {
		return err
	}
	msg = msg[i+1:]
	msg = strings.TrimSpace(msg)
	switch msg {
	case "NO_ERROR", "PROTOCOL_ERROR", "INTERNAL_ERROR", "FLOW_CONTROL_ERROR",
		"SETTINGS_TIMEOUT", "FRAME_SIZE_ERROR", "COMPRESSION_ERROR", "CONNECT_ERROR":
		return NewError(CodeInternal, err)
	case "REFUSED_STREAM":
		return NewError(CodeUnavailable, err)
	case "CANCEL":
		if deadline, ok := ctx.Deadline(); ok && time.Now().After(deadline) {
			// Some server implementations will cancel the HTTP/2 stream with
			// a RST_STREAM frame when they observe that the client's deadline
			// has elapsed.
			// We don't inspect ctx.Err() because we could be racing with the
			// timer goroutine that is setting it. But there is no race when
			// directly inspecting the context's deadline. In fact, if we get
			// here, we have likely already examined ctx.Err() in a prior call
			// to wrapIfContextError but observed a nil error and then fell
			// through to here.
			return NewError(CodeDeadlineExceeded, err)
		}
		return NewError(CodeCanceled, err)
	case "ENHANCE_YOUR_CALM":
		return NewError(CodeResourceExhausted, fmt.Errorf("bandwidth exhausted: %w", err))
	case "INADEQUATE_SECURITY":
		return NewError(CodePermissionDenied, fmt.Errorf("transport protocol insecure: %w", err))
	default:
		return err
	}
}

// wrapIfMaxBytesError wraps errors returned reading from a http.MaxBytesHandler
// whose limit has been exceeded.
func wrapIfMaxBytesError(err error, tmpl string, args ...any) error {
	if err == nil {
		return nil
	}
	if _, ok := asError(err); ok {
		return err
	}
	var maxBytesErr *http.MaxBytesError
	if ok := errors.As(err, &maxBytesErr); !ok {
		return err
	}
	prefix := fmt.Sprintf(tmpl, args...)
	return errorf(CodeResourceExhausted, "%s: exceeded %d byte http.MaxBytesReader limit", prefix, maxBytesErr.Limit)
}

func typeNameForURL(url string) string {
	return url[strings.LastIndexByte(url, '/')+1:]
}
