// Copyright The OpenTelemetry 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 semconv // import "go.opentelemetry.io/otel/semconv/v1.7.0"

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

	"go.opentelemetry.io/otel/trace"

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

// HTTP scheme attributes.
var (
	HTTPSchemeHTTP  = HTTPSchemeKey.String("http")
	HTTPSchemeHTTPS = HTTPSchemeKey.String("https")
)

// 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 NetAttributesFromHTTPRequest(network string, request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{}

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

	peerIP, peerName, peerPort := hostIPNamePort(request.RemoteAddr)
	if peerIP != "" {
		attrs = append(attrs, NetPeerIPKey.String(peerIP))
	}
	if peerName != "" {
		attrs = append(attrs, NetPeerNameKey.String(peerName))
	}
	if peerPort != 0 {
		attrs = append(attrs, 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, NetHostIPKey.String(hostIP))
	}
	if hostName != "" {
		attrs = append(attrs, NetHostNameKey.String(hostName))
	}
	if hostPort != 0 {
		attrs = append(attrs, 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 EndUserAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue {
	if username, _, ok := request.BasicAuth(); ok {
		return []attribute.KeyValue{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 HTTPClientAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{}

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

	// 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, HTTPURLKey.String(request.URL.String()))

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

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

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

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

func 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, HTTPSchemeHTTPS)
	} else {
		attrs = append(attrs, HTTPSchemeHTTP)
	}

	if request.Host != "" {
		attrs = append(attrs, HTTPHostKey.String(request.Host))
	} else if request.URL != nil && request.URL.Host != "" {
		attrs = append(attrs, 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, HTTPFlavorKey.String(flavor))
	}

	return attrs
}

// HTTPServerMetricAttributesFromHTTPRequest generates low-cardinality attributes
// to be used with server-side HTTP metrics.
func HTTPServerMetricAttributesFromHTTPRequest(serverName string, request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{}
	if serverName != "" {
		attrs = append(attrs, HTTPServerNameKey.String(serverName))
	}
	return append(attrs, 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 HTTPServerAttributesFromHTTPRequest(serverName, route string, request *http.Request) []attribute.KeyValue {
	attrs := []attribute.KeyValue{
		HTTPMethodKey.String(request.Method),
		HTTPTargetKey.String(request.RequestURI),
	}

	if serverName != "" {
		attrs = append(attrs, HTTPServerNameKey.String(serverName))
	}
	if route != "" {
		attrs = append(attrs, HTTPRouteKey.String(route))
	}
	if values, ok := request.Header["X-Forwarded-For"]; ok && len(values) > 0 {
		if addresses := strings.SplitN(values[0], ",", 2); len(addresses) > 0 {
			attrs = append(attrs, HTTPClientIPKey.String(addresses[0]))
		}
	}

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

// HTTPAttributesFromHTTPStatusCode generates attributes of the http
// namespace as specified by the OpenTelemetry specification for a
// span.
func HTTPAttributesFromHTTPStatusCode(code int) []attribute.KeyValue {
	attrs := []attribute.KeyValue{
		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, ""
}

// 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
}
