package grpc

import (
	"context"
	"fmt"
	"net"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	grpcmetadata "google.golang.org/grpc/metadata"
)

// copied from https://github.com/tidbcloud/core-svc
const (
	httpMethod         = "x-http-method"
	httpPATH           = "x-http-path"
	httpURI            = "x-http-uri"
	httpAuthorization  = "authorization"
	httpXAuthType      = "x-auth-type"
	httpXAuthContent   = "x-auth-content"
	xForwardedFor      = "x-forwarded-for"
	xForwardedHost     = "x-forwarded-host"
	grpcMetaDataPrefix = "grpc-metadata-"
)

func NewContext(ctx *gin.Context) context.Context {
	md := map[string]string{
		httpMethod: ctx.Request.Method,
		httpPATH:   ctx.Request.URL.Path,
		httpURI:    ctx.Request.RequestURI,
	}

	for header := range ctx.Request.Header {
		header = strings.ToLower(header)

		// set authorization header
		if header == httpAuthorization || header == httpXAuthType || header == httpXAuthContent {
			if h := ctx.Request.Header.Get(header); h != "" {
				md[header] = h
			}
			continue
		}

		// grpc metadata
		if strings.HasPrefix(header, grpcMetaDataPrefix) {
			if h := ctx.Request.Header.Get(header); h != "" {
				md[header[len(grpcMetaDataPrefix):]] = h
			}
			continue
		}

		// http header metadata
		if isPermanentHTTPHeader(header) {
			if !isValidGRPCMetadataKey(header) || !isValidGRPCMetadataTextValue(ctx.Request.Header.Get(header)) {
				tilog.Warn(
					ctx,
					"invalid grpc metadata key/value",
					zap.String("header", header),
					zap.String("value", ctx.Request.Header.Get(header)),
				)
			} else {
				if h := ctx.Request.Header.Get(header); h != "" {
					md[header] = h
				}
			}
			continue
		}
	}

	if host := ctx.Request.Header.Get(xForwardedHost); host != "" {
		md[xForwardedHost] = host
	} else if ctx.Request.Host != "" {
		md[xForwardedHost] = ctx.Request.Host
	}

	if addr := ctx.Request.RemoteAddr; addr != "" {
		if remoteIP, _, err := net.SplitHostPort(addr); err == nil {
			if fwd := ctx.Request.Header.Get(xForwardedFor); fwd == "" {
				md[xForwardedFor] = remoteIP
			} else {
				md[xForwardedFor] = fmt.Sprintf("%s, %s", fwd, remoteIP)
			}
		}
	}

	// tilog.Info(ctx.Request.Context(), "grpc metadata", zap.Any("metadata", md))
	return grpcmetadata.NewOutgoingContext(ctx.Request.Context(), grpcmetadata.New(md))
}

// isPermanentHTTPHeader checks whether hdr belongs to the list of
// permanent request headers maintained by IANA.
// http://www.iana.org/assignments/message-headers/message-headers.xml
func isPermanentHTTPHeader(hdr string) bool {
	switch hdr {
	case
		"accept",
		"accept-charset",
		"accept-language",
		"accept-ranges",
		"authorization",
		"cache-control",
		"content-type",
		"cookie",
		"date",
		"expect",
		"from",
		"host",
		"if-match",
		"if-modified-since",
		"if-mone-match",
		"if-schedule-tag-match",
		"if-unmodified-since",
		"max-forwards",
		"origin",
		"pragma",
		"referer",
		"user-agent",
		"via",
		"warning":
		return true
	}
	return false
}

func isValidGRPCMetadataKey(key string) bool {
	// Must be a valid gRPC "Header-Name" as defined here:
	//   https://github.com/grpc/grpc/blob/4b05dc88b724214d0c725c8e7442cbc7a61b1374/doc/PROTOCOL-HTTP2.md
	// This means 0-9 a-z _ - .
	// Only lowercase letters are valid in the wire protocol, but the client library will normalize
	// uppercase ASCII to lowercase, so uppercase ASCII is also acceptable.
	bytes := []byte(key) // gRPC validates strings on the byte level, not Unicode.
	for _, ch := range bytes {
		validLowercaseLetter := ch >= 'a' && ch <= 'z'
		validUppercaseLetter := ch >= 'A' && ch <= 'Z'
		validDigit := ch >= '0' && ch <= '9'
		validOther := ch == '.' || ch == '-' || ch == '_'
		if !validLowercaseLetter && !validUppercaseLetter && !validDigit && !validOther {
			return false
		}
	}
	return true
}

func isValidGRPCMetadataTextValue(textValue string) bool {
	// Must be a valid gRPC "ASCII-Value" as defined here:
	//   https://github.com/grpc/grpc/blob/4b05dc88b724214d0c725c8e7442cbc7a61b1374/doc/PROTOCOL-HTTP2.md
	// This means printable ASCII (including/plus spaces); 0x20 to 0x7E inclusive.
	bytes := []byte(textValue) // gRPC validates strings on the byte level, not Unicode.
	for _, ch := range bytes {
		if ch < 0x20 || ch > 0x7E {
			return false
		}
	}
	return true
}
