// 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.4.0"

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

	"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)
	}

	peerName, peerIP, peerPort := "", "", 0
	{
		hostPart := request.RemoteAddr
		portPart := ""
		if idx := strings.LastIndex(hostPart, ":"); idx >= 0 {
			hostPart = request.RemoteAddr[:idx]
			portPart = request.RemoteAddr[idx+1:]
		}
		if hostPart != "" {
			if ip := net.ParseIP(hostPart); ip != nil {
				peerIP = ip.String()
			} else {
				peerName = hostPart
			}

			if portPart != "" {
				numPort, err := strconv.ParseUint(portPart, 10, 16)
				if err == nil {
					peerPort = (int)(numPort)
				} else {
					peerName, peerIP = "", ""
				}
			}
		}
	}
	if peerName != "" {
		attrs = append(attrs, NetPeerNameKey.String(peerName))
	}
	if peerIP != "" {
		attrs = append(attrs, NetPeerIPKey.String(peerIP))
	}
	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} {
		hostPart := ""
		if idx := strings.LastIndex(someHost, ":"); idx >= 0 {
			strPort := someHost[idx+1:]
			numPort, err := strconv.ParseUint(strPort, 10, 16)
			if err == nil {
				hostPort = (int)(numPort)
			}
			hostPart = someHost[:idx]
		} else {
			hostPart = someHost
		}
		if hostPart != "" {
			ip := net.ParseIP(hostPart)
			if ip != nil {
				hostIP = ip.String()
			} else {
				hostName = hostPart
			}
			break
		} else {
			hostPort = 0
		}
	}
	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
}

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

	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 {
		attrs = append(attrs, HTTPClientIPKey.String(values[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, ""
}

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