// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package internal // import "go.opentelemetry.io/otel/semconv/internal"

import (
	"fmt"
	"net"
	"net/http"
	"strconv"
	"strings"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
)

// SemanticConventions are the semantic convention values defined for a
// version of the OpenTelemetry specification.
type SemanticConventions struct {
	EnduserIDKey                attribute.Key
	HTTPClientIPKey             attribute.Key
	HTTPFlavorKey               attribute.Key
	HTTPHostKey                 attribute.Key
	HTTPMethodKey               attribute.Key
	HTTPRequestContentLengthKey attribute.Key
	HTTPRouteKey                attribute.Key
	HTTPSchemeHTTP              attribute.KeyValue
	HTTPSchemeHTTPS             attribute.KeyValue
	HTTPServerNameKey           attribute.Key
	HTTPStatusCodeKey           attribute.Key
	HTTPTargetKey               attribute.Key
	HTTPURLKey                  attribute.Key
	HTTPUserAgentKey            attribute.Key
	NetHostIPKey                attribute.Key
	NetHostNameKey              attribute.Key
	NetHostPortKey              attribute.Key
	NetPeerIPKey                attribute.Key
	NetPeerNameKey              attribute.Key
	NetPeerPortKey              attribute.Key
	NetTransportIP              attribute.KeyValue
	NetTransportOther           attribute.KeyValue
	NetTransportTCP             attribute.KeyValue
	NetTransportUDP             attribute.KeyValue
	NetTransportUnix            attribute.KeyValue
}

// NetAttributesFromHTTPRequest generates attributes of the net
// namespace as specified by the OpenTelemetry specification for a
// span.  The network parameter is a string that net.Dial function
// from standard library can understand.
func (sc *SemanticConventions) NetAttributesFromHTTPRequest(network string, request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{}

	switch network {
	case "tcp", "tcp4", "tcp6":
		attrs = append(attrs, sc.NetTransportTCP)
	case "udp", "udp4", "udp6":
		attrs = append(attrs, sc.NetTransportUDP)
	case "ip", "ip4", "ip6":
		attrs = append(attrs, sc.NetTransportIP)
	case "unix", "unixgram", "unixpacket":
		attrs = append(attrs, sc.NetTransportUnix)
	default:
		attrs = append(attrs, sc.NetTransportOther)
	}

	peerIP, peerName, peerPort := hostIPNamePort(request.RemoteAddr)
	if peerIP != "" {
		attrs = append(attrs, sc.NetPeerIPKey.String(peerIP))
	}
	if peerName != "" {
		attrs = append(attrs, sc.NetPeerNameKey.String(peerName))
	}
	if peerPort != 0 {
		attrs = append(attrs, sc.NetPeerPortKey.Int(peerPort))
	}

	hostIP, hostName, hostPort := "", "", 0
	for _, someHost := range []string{request.Host, request.Header.Get("Host"), request.URL.Host} {
		hostIP, hostName, hostPort = hostIPNamePort(someHost)
		if hostIP != "" || hostName != "" || hostPort != 0 {
			break
		}
	}
	if hostIP != "" {
		attrs = append(attrs, sc.NetHostIPKey.String(hostIP))
	}
	if hostName != "" {
		attrs = append(attrs, sc.NetHostNameKey.String(hostName))
	}
	if hostPort != 0 {
		attrs = append(attrs, sc.NetHostPortKey.Int(hostPort))
	}

	return attrs
}

// hostIPNamePort extracts the IP address, name and (optional) port from hostWithPort.
// It handles both IPv4 and IPv6 addresses. If the host portion is not recognized
// as a valid IPv4 or IPv6 address, the `ip` result will be empty and the
// host portion will instead be returned in `name`.
func hostIPNamePort(hostWithPort string) (ip string, name string, port int) {
	var (
		hostPart, portPart string
		parsedPort         uint64
		err                error
	)
	if hostPart, portPart, err = net.SplitHostPort(hostWithPort); err != nil {
		hostPart, portPart = hostWithPort, ""
	}
	if parsedIP := net.ParseIP(hostPart); parsedIP != nil {
		ip = parsedIP.String()
	} else {
		name = hostPart
	}
	if parsedPort, err = strconv.ParseUint(portPart, 10, 16); err == nil {
		port = int(parsedPort)
	}
	return
}

// EndUserAttributesFromHTTPRequest generates attributes of the
// enduser namespace as specified by the OpenTelemetry specification
// for a span.
func (sc *SemanticConventions) EndUserAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue {
	if username, _, ok := request.BasicAuth(); ok {
		return []attribute.KeyValue{sc.EnduserIDKey.String(username)}
	}
	return nil
}

// HTTPClientAttributesFromHTTPRequest generates attributes of the
// http namespace as specified by the OpenTelemetry specification for
// a span on the client side.
func (sc *SemanticConventions) HTTPClientAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{}

	// remove any username/password info that may be in the URL
	// before adding it to the attributes
	userinfo := request.URL.User
	request.URL.User = nil

	attrs = append(attrs, sc.HTTPURLKey.String(request.URL.String()))

	// restore any username/password info that was removed
	request.URL.User = userinfo

	return append(attrs, sc.httpCommonAttributesFromHTTPRequest(request)...)
}

func (sc *SemanticConventions) httpCommonAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{}
	if ua := request.UserAgent(); ua != "" {
		attrs = append(attrs, sc.HTTPUserAgentKey.String(ua))
	}
	if request.ContentLength > 0 {
		attrs = append(attrs, sc.HTTPRequestContentLengthKey.Int64(request.ContentLength))
	}

	return append(attrs, sc.httpBasicAttributesFromHTTPRequest(request)...)
}

func (sc *SemanticConventions) httpBasicAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue {
	// as these attributes are used by HTTPServerMetricAttributesFromHTTPRequest, they should be low-cardinality
	attrs := []attribute.KeyValue{}

	if request.TLS != nil {
		attrs = append(attrs, sc.HTTPSchemeHTTPS)
	} else {
		attrs = append(attrs, sc.HTTPSchemeHTTP)
	}

	if request.Host != "" {
		attrs = append(attrs, sc.HTTPHostKey.String(request.Host))
	} else if request.URL != nil && request.URL.Host != "" {
		attrs = append(attrs, sc.HTTPHostKey.String(request.URL.Host))
	}

	flavor := ""
	if request.ProtoMajor == 1 {
		flavor = fmt.Sprintf("1.%d", request.ProtoMinor)
	} else if request.ProtoMajor == 2 {
		flavor = "2"
	}
	if flavor != "" {
		attrs = append(attrs, sc.HTTPFlavorKey.String(flavor))
	}

	if request.Method != "" {
		attrs = append(attrs, sc.HTTPMethodKey.String(request.Method))
	} else {
		attrs = append(attrs, sc.HTTPMethodKey.String(http.MethodGet))
	}

	return attrs
}

// HTTPServerMetricAttributesFromHTTPRequest generates low-cardinality attributes
// to be used with server-side HTTP metrics.
func (sc *SemanticConventions) HTTPServerMetricAttributesFromHTTPRequest(serverName string, request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{}
	if serverName != "" {
		attrs = append(attrs, sc.HTTPServerNameKey.String(serverName))
	}
	return append(attrs, sc.httpBasicAttributesFromHTTPRequest(request)...)
}

// HTTPServerAttributesFromHTTPRequest generates attributes of the
// http namespace as specified by the OpenTelemetry specification for
// a span on the server side. Currently, only basic authentication is
// supported.
func (sc *SemanticConventions) HTTPServerAttributesFromHTTPRequest(serverName, route string, request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{
		sc.HTTPTargetKey.String(request.RequestURI),
	}

	if serverName != "" {
		attrs = append(attrs, sc.HTTPServerNameKey.String(serverName))
	}
	if route != "" {
		attrs = append(attrs, sc.HTTPRouteKey.String(route))
	}
	if values := request.Header["X-Forwarded-For"]; len(values) > 0 {
		addr := values[0]
		if i := strings.Index(addr, ","); i > 0 {
			addr = addr[:i]
		}
		attrs = append(attrs, sc.HTTPClientIPKey.String(addr))
	}

	return append(attrs, sc.httpCommonAttributesFromHTTPRequest(request)...)
}

// HTTPAttributesFromHTTPStatusCode generates attributes of the http
// namespace as specified by the OpenTelemetry specification for a
// span.
func (sc *SemanticConventions) HTTPAttributesFromHTTPStatusCode(code int) []attribute.KeyValue {
	attrs := []attribute.KeyValue{
		sc.HTTPStatusCodeKey.Int(code),
	}
	return attrs
}

type codeRange struct {
	fromInclusive int
	toInclusive   int
}

func (r codeRange) contains(code int) bool {
	return r.fromInclusive <= code && code <= r.toInclusive
}

var validRangesPerCategory = map[int][]codeRange{
	1: {
		{http.StatusContinue, http.StatusEarlyHints},
	},
	2: {
		{http.StatusOK, http.StatusAlreadyReported},
		{http.StatusIMUsed, http.StatusIMUsed},
	},
	3: {
		{http.StatusMultipleChoices, http.StatusUseProxy},
		{http.StatusTemporaryRedirect, http.StatusPermanentRedirect},
	},
	4: {
		{http.StatusBadRequest, http.StatusTeapot}, // yes, teapot is so useful…
		{http.StatusMisdirectedRequest, http.StatusUpgradeRequired},
		{http.StatusPreconditionRequired, http.StatusTooManyRequests},
		{http.StatusRequestHeaderFieldsTooLarge, http.StatusRequestHeaderFieldsTooLarge},
		{http.StatusUnavailableForLegalReasons, http.StatusUnavailableForLegalReasons},
	},
	5: {
		{http.StatusInternalServerError, http.StatusLoopDetected},
		{http.StatusNotExtended, http.StatusNetworkAuthenticationRequired},
	},
}

// SpanStatusFromHTTPStatusCode generates a status code and a message
// as specified by the OpenTelemetry specification for a span.
func SpanStatusFromHTTPStatusCode(code int) (codes.Code, string) {
	spanCode, valid := validateHTTPStatusCode(code)
	if !valid {
		return spanCode, fmt.Sprintf("Invalid HTTP status code %d", code)
	}
	return spanCode, ""
}

// SpanStatusFromHTTPStatusCodeAndSpanKind generates a status code and a message
// as specified by the OpenTelemetry specification for a span.
// Exclude 4xx for SERVER to set the appropriate status.
func SpanStatusFromHTTPStatusCodeAndSpanKind(code int, spanKind trace.SpanKind) (codes.Code, string) {
	spanCode, valid := validateHTTPStatusCode(code)
	if !valid {
		return spanCode, fmt.Sprintf("Invalid HTTP status code %d", code)
	}
	category := code / 100
	if spanKind == trace.SpanKindServer && category == 4 {
		return codes.Unset, ""
	}
	return spanCode, ""
}

// validateHTTPStatusCode validates the HTTP status code and returns
// corresponding span status code. If the `code` is not a valid HTTP status
// code, returns span status Error and false.
func validateHTTPStatusCode(code int) (codes.Code, bool) {
	category := code / 100
	ranges, ok := validRangesPerCategory[category]
	if !ok {
		return codes.Error, false
	}
	ok = false
	for _, crange := range ranges {
		ok = crange.contains(code)
		if ok {
			break
		}
	}
	if !ok {
		return codes.Error, false
	}
	if category > 0 && category < 4 {
		return codes.Unset, true
	}
	return codes.Error, true
}
