// Copyright 2021-2025 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"
	"net/http"
)

// CallInfo represents information relevant to an RPC call.
type CallInfo interface {
	// Spec returns a description of this call.
	Spec() Spec
	// Peer describes the other party for this call.
	Peer() Peer
	// RequestHeader returns the HTTP headers for this request. Headers beginning with
	// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC
	// protocols: applications may read them but shouldn't write them.
	RequestHeader() http.Header
	// ResponseHeader returns the HTTP headers for this response. Headers beginning with
	// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC
	// protocols: applications may read them but shouldn't write them.
	// On the client side, this method returns nil before
	// the call is actually made. After the call is made, for streaming operations,
	// this method will block for the server to actually return response headers.
	ResponseHeader() http.Header
	// ResponseTrailer returns the trailers for this response. Depending on the underlying
	// RPC protocol, trailers may be sent as HTTP trailers or a protocol-specific
	// block of in-body metadata.
	//
	// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the
	// Connect and gRPC protocols: applications may read them but shouldn't write
	// them.
	//
	// On the client side, this method returns nil before the call is actually made.
	// After the call is made, for streaming operations, this method will block
	// for the server to actually return response trailers.
	ResponseTrailer() http.Header
	// HTTPMethod returns the HTTP method for this request. This is nearly always
	// POST, but side-effect-free unary RPCs could be made via a GET.
	//
	// On a newly created request, via NewRequest, this will return the empty
	// string until the actual request is actually sent and the HTTP method
	// determined. This means that client interceptor functions will see the
	// empty string until *after* they delegate to the handler they wrapped. It
	// is even possible for this to return the empty string after such delegation,
	// if the request was never actually sent to the server (and thus no
	// determination ever made about the HTTP method).
	HTTPMethod() string

	internalOnly()
}

// NewClientContext creates a new client (i.e. outgoing) context for use from a
// client. When the returned context is passed to RPCs, the returned call info
// can be used to set request metadata before the RPC is invoked and to inspect
// response metadata after the RPC completes.
//
// The returned context may be re-used across RPCs as long as they are
// not concurrent. Results of all CallInfo methods other than
// RequestHeader() are undefined if the context is used with concurrent RPCs.
func NewClientContext(ctx context.Context) (context.Context, CallInfo) {
	info := &clientCallInfo{}
	return context.WithValue(ctx, clientCallInfoContextKey{}, info), info
}

// CallInfoForHandlerContext returns the CallInfo for the given handler (i.e. incoming) context, if there is one.
func CallInfoForHandlerContext(ctx context.Context) (CallInfo, bool) {
	value, ok := ctx.Value(handlerCallInfoContextKey{}).(CallInfo)
	return value, ok
}

// handlerCallInfo is a CallInfo implementation used for unary handlers.
type handlerCallInfo struct {
	spec            Spec
	peer            Peer
	method          string
	requestHeader   http.Header
	responseHeader  http.Header
	responseTrailer http.Header
}

func (c *handlerCallInfo) Spec() Spec {
	return c.spec
}

func (c *handlerCallInfo) Peer() Peer {
	return c.peer
}

func (c *handlerCallInfo) RequestHeader() http.Header {
	if c.requestHeader == nil {
		c.requestHeader = make(http.Header)
	}
	return c.requestHeader
}

func (c *handlerCallInfo) ResponseHeader() http.Header {
	if c.responseHeader == nil {
		c.responseHeader = make(http.Header)
	}
	return c.responseHeader
}

func (c *handlerCallInfo) ResponseTrailer() http.Header {
	if c.responseTrailer == nil {
		c.responseTrailer = make(http.Header)
	}
	return c.responseTrailer
}

func (c *handlerCallInfo) HTTPMethod() string {
	return c.method
}

// internalOnly implements CallInfo.
func (c *handlerCallInfo) internalOnly() {}

// streamingHandlerCallInfo is a CallInfo implementation used for streaming RPC handlers.
type streamingHandlerCallInfo struct {
	conn StreamingHandlerConn
}

func (c *streamingHandlerCallInfo) Spec() Spec {
	return c.conn.Spec()
}

func (c *streamingHandlerCallInfo) Peer() Peer {
	return c.conn.Peer()
}

func (c *streamingHandlerCallInfo) RequestHeader() http.Header {
	return c.conn.RequestHeader()
}

func (c *streamingHandlerCallInfo) ResponseHeader() http.Header {
	return c.conn.ResponseHeader()
}

func (c *streamingHandlerCallInfo) ResponseTrailer() http.Header {
	return c.conn.ResponseTrailer()
}

func (c *streamingHandlerCallInfo) HTTPMethod() string {
	// All stream calls are POSTs
	return http.MethodPost
}

// internalOnly implements CallInfo.
func (c *streamingHandlerCallInfo) internalOnly() {}

// clientCallInfo is a CallInfo implementation used for clients.
type clientCallInfo struct {
	responseSource

	spec          Spec
	peer          Peer
	method        string
	requestHeader http.Header
}

func (c *clientCallInfo) Spec() Spec {
	return c.spec
}

func (c *clientCallInfo) Peer() Peer {
	return c.peer
}

func (c *clientCallInfo) RequestHeader() http.Header {
	if c.requestHeader == nil {
		c.requestHeader = make(http.Header)
	}
	return c.requestHeader
}

func (c *clientCallInfo) ResponseHeader() http.Header {
	if c.responseSource == nil {
		return nil
	}
	return c.responseSource.ResponseHeader()
}

func (c *clientCallInfo) ResponseTrailer() http.Header {
	if c.responseSource == nil {
		return nil
	}
	return c.responseSource.ResponseTrailer()
}

func (c *clientCallInfo) HTTPMethod() string {
	return c.method
}

// internalOnly implements CallInfo.
func (c *clientCallInfo) internalOnly() {}

// clientCallInfoContextKey is the key used to store client call info in context.
type clientCallInfoContextKey struct{}

// sentinelContextKey is the key used to store a copy of client call info in context
// when a request is made.
// Each step in an interceptor chain compares the actual call info with the
// sentinel call info. If the two values are different, the request will
// return an error in the interceptor.
// This protects against changing the call info in interceptors, which is prohibited
// as it would allow users to modify call info mid-flight independent of the actual
// request or response.
// Users who wish to modify call info data such as headers and trailers should instead
// use Connect [Request] and [Response] wrapper types.
type sentinelContextKey struct{}

// handlerCallInfoContextKey is the key used to store handler call info in context.
type handlerCallInfoContextKey struct{}

// responseSource indicates a type that manages response headers and trailers.
type responseSource interface {
	ResponseHeader() http.Header
	ResponseTrailer() http.Header
}

// clientCallInfoForContext gets the call info from a client/outgoing context.
func clientCallInfoForContext(ctx context.Context) (*clientCallInfo, bool) {
	info, ok := ctx.Value(clientCallInfoContextKey{}).(*clientCallInfo)
	return info, ok
}

// newHandlerContext creates a new handler/incoming context.
func newHandlerContext(ctx context.Context, info CallInfo) context.Context {
	return context.WithValue(ctx, handlerCallInfoContextKey{}, info)
}
