package httpz

import (
	"iter"
	"maps"
	"mime"
	"net/http"
	"net/textproto"
	"net/url"
	"slices"
	"strings"
)

// These headers must be in their canonical form. Only add headers used in production code, don't bother with tests.
// Make sure to update the expectedNumberOfDefinedHeaders constant in the test file when adding or removing a header constant.
const (
	ConnectionHeader                      = "Connection" // https://datatracker.ietf.org/doc/html/rfc9110#section-7.6.1
	ProxyConnectionHeader                 = "Proxy-Connection"
	KeepAliveHeader                       = "Keep-Alive"
	HostHeader                            = "Host"
	ProxyAuthenticateHeader               = "Proxy-Authenticate"
	ProxyAuthorizationHeader              = "Proxy-Authorization"
	TeHeader                              = "Te"      // canonicalized version of "TE"
	TrailerHeader                         = "Trailer" // not Trailers as per rfc2616; See errata https://www.rfc-editor.org/errata_search.php?eid=4522
	TransferEncodingHeader                = "Transfer-Encoding"
	UpgradeHeader                         = "Upgrade" // https://datatracker.ietf.org/doc/html/rfc9110#section-7.8
	UserAgentHeader                       = "User-Agent"
	AuthorizationHeader                   = "Authorization" // https://datatracker.ietf.org/doc/html/rfc9110#section-11.6.2
	CookieHeader                          = "Cookie"        // https://datatracker.ietf.org/doc/html/rfc6265#section-5.4
	SetCookieHeader                       = "Set-Cookie"    // https://datatracker.ietf.org/doc/html/rfc6265#section-4.1
	ContentTypeHeader                     = "Content-Type"  // https://datatracker.ietf.org/doc/html/rfc9110#section-8.3
	AcceptHeader                          = "Accept"        // https://datatracker.ietf.org/doc/html/rfc9110#section-12.5.1
	ServerHeader                          = "Server"        // https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.4
	ViaHeader                             = "Via"           // https://datatracker.ietf.org/doc/html/rfc9110#section-7.6.3
	GitlabAgentIDHeader                   = "Gitlab-Agent-Id"
	GitlabAgentIDQueryParam               = "gitlab-agent-id"
	GitlabUnauthorizedHeader              = "Gitlab-Unauthorized"
	GitlabAgentVersionHeader              = "Gitlab-Agent-Version"
	CSRFTokenHeader                       = "X-Csrf-Token"                     //nolint: gosec
	CSRFTokenQueryParam                   = "gitlab-csrf-token"                //nolint: gosec
	AccessControlAllowOriginHeader        = "Access-Control-Allow-Origin"      // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
	AccessControlAllowHeadersHeader       = "Access-Control-Allow-Headers"     // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
	AccessControlAllowCredentialsHeader   = "Access-Control-Allow-Credentials" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
	AccessControlAllowMethodsHeader       = "Access-Control-Allow-Methods"     // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
	AccessControlMaxAgeHeader             = "Access-Control-Max-Age"           // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
	AccessControlRequestHeadersHeader     = "Access-Control-Request-Headers"   // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers
	VaryHeader                            = "Vary"                             // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
	OriginHeader                          = "Origin"                           // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin
	RequestIDHeader                       = "X-Request-Id"
	WarningHeader                         = "Warning"                // https://datatracker.ietf.org/doc/html/rfc2068#section-14.45
	SecWebSocketProtocolHeader            = "Sec-Websocket-Protocol" // https://datatracker.ietf.org/doc/html/rfc6455#section-11.3.4
	NELHeader                             = "Nel"                    // https://w3c.github.io/network-error-logging/#dfn-nel
	ReportToHeader                        = "Report-To"              // https://w3c.github.io/network-error-logging/#the-report_to-member
	SignatureHeader                       = "X-Signature"
	XContentTypeOptionsHeader             = "X-Content-Type-Options"       // https://fetch.spec.whatwg.org/#x-content-type-options-header
	PragmaHeader                          = "Pragma"                       // https://datatracker.ietf.org/doc/html/rfc7234#section-5.4
	ExpiresHeader                         = "Expires"                      // https://datatracker.ietf.org/doc/html/rfc7234#section-5.3
	SurrogateControlHeader                = "Surrogate-Control"            // https://www.rfc-editor.org/rfc/rfc4229.html#section-2.1.102
	CloudFlareCDNCacheControlHeader       = "Cloudflare-Cdn-Cache-Control" // https://developers.cloudflare.com/cache/concepts/cdn-cache-control/
	CDNCacheControlHeader                 = "Cdn-Cache-Control"            // https://datatracker.ietf.org/doc/html/draft-cdn-control-header-00
	XAccelRedirectHeader                  = "X-Accel-Redirect"             // https://nginx.org/en/docs/http/ngx_http_proxy_module.html
	GitlabFeatureFlagHeader               = "Gitlab-Feature-Flag"
	ContentLengthHeader                   = "Content-Length"                      // https://datatracker.ietf.org/doc/html/rfc9110#name-content-length
	DateHeader                            = "Date"                                // https://datatracker.ietf.org/doc/html/rfc9110#name-date
	AuditIDHeader                         = "Audit-Id"                            // https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/
	RetryAfterHeader                      = "Retry-After"                         // https://datatracker.ietf.org/doc/html/rfc9110#name-retry-after
	LocationHeader                        = "Location"                            // https://datatracker.ietf.org/doc/html/rfc9110#name-location
	XStreamProtocolVersionHeader          = "X-Stream-Protocol-Version"           // https://github.com/kubernetes/kubernetes/blob/80b0f696a645a752c3e8981565472751d8ad2560/staging/src/k8s.io/apimachinery/pkg/util/httpstream/httpstream.go#L31
	XAcceptedStreamProtocolVersionsHeader = "X-Accepted-Stream-Protocol-Versions" // https://github.com/kubernetes/kubernetes/blob/80b0f696a645a752c3e8981565472751d8ad2560/staging/src/k8s.io/apimachinery/pkg/util/httpstream/httpstream.go#L32
	ContentEncodingHeader                 = "Content-Encoding"                    // https://datatracker.ietf.org/doc/html/rfc9110#name-content-encoding
	SecWebSocketExtensionsHeader          = "Sec-Websocket-Extensions"            // https://datatracker.ietf.org/doc/html/rfc6455#section-11.3.2
	SecWebSocketAcceptHeader              = "Sec-Websocket-Accept"                // https://datatracker.ietf.org/doc/html/rfc6455#section-11.3.3
	SecWebSocketVersionHeader             = "Sec-Websocket-Version"               // https://datatracker.ietf.org/doc/html/rfc6455#section-11.3.5
	AcceptRangesHeader                    = "Accept-Ranges"                       // https://datatracker.ietf.org/doc/html/rfc9110#field.accept-ranges
	ContentRangeHeader                    = "Content-Range"                       // https://datatracker.ietf.org/doc/html/rfc9110#name-content-range
	LastModifiedHeader                    = "Last-Modified"                       // https://datatracker.ietf.org/doc/html/rfc9110#name-last-modified
	CacheControlHeader                    = "Cache-Control"                       // https://datatracker.ietf.org/doc/html/rfc9111#name-cache-control
	EtagHeader                            = "Etag"                                // https://datatracker.ietf.org/doc/html/rfc9110#field.etag
	StrictTransportSecurityHeader         = "Strict-Transport-Security"           // https://datatracker.ietf.org/doc/html/rfc6797#section-6.1
	XForwardedProtoHeader                 = "X-Forwarded-Proto"                   // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto

	// TLSNextProtoH2 is the NPN/ALPN protocol negotiated during HTTP/2's TLS setup.
	TLSNextProtoH2 = "h2"
	TLSNextProtoH1 = "http/1.1"

	// H2ClientPreface is the string that must be sent by new
	// connections from clients.
	H2ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
)

// RemoveConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
// See https://datatracker.ietf.org/doc/html/rfc7230#section-6.1
func RemoveConnectionHeaders(h http.Header) {
	for v := range HeaderValues(h, ConnectionHeader) {
		// must use .Del() because connection options are case-insensitive and are likely in lower case, not in canonical case
		h.Del(v)
	}
}

// RemoveHeaderValue removes all values in for given header key.
// If no values are left for that key, the key is removed entirely.
// All values for the given key will be merged into a single appearance of key in h.
func RemoveHeaderValue(h http.Header, key string, f func(s string) bool) {
	var keep []string
	for v := range HeaderValues(h, key) {
		if !f(v) {
			keep = append(keep, v)
		}
	}

	switch {
	case len(keep) > 0:
		h[key] = keep
	default:
		delete(h, key)
	}
}

func HasHeaderValue(h http.Header, key, value string) bool {
	for v := range HeaderValues(h, key) {
		if strings.EqualFold(v, value) {
			return true
		}
	}
	return false
}

// HeaderValues returns an iterator to iterate all values for the given header key.
// The key must be in canonical format.
func HeaderValues(h http.Header, key string) iter.Seq[string] {
	return func(yield func(string) bool) {
		for _, values := range h[key] {
			for v := range strings.SplitSeq(values, ",") {
				v = textproto.TrimString(v)
				if v == "" {
					continue
				}
				if !yield(v) {
					return
				}
			}
		}
	}
}

func IsContentType(actual string, expected ...string) bool {
	parsed, _, err := mime.ParseMediaType(actual)
	if err != nil {
		return false
	}
	return slices.Contains(expected, parsed)
}

func MergeURLPathAndQuery(baseURL *url.URL, extraPath string, query url.Values) string {
	u := *baseURL
	u.Path = joinURLPaths(u.Path, extraPath)

	if len(query) == 0 {
		// Nothing to do
	} else {
		// Merge queries
		var q url.Values
		if u.RawQuery == "" {
			// Nothing to merge into, just use query
			q = query
		} else {
			uq := u.Query()
			maps.Copy(uq, query)
			q = uq
		}
		u.RawQuery = q.Encode()
	}
	return u.String()
}

func joinURLPaths(head, tail string) string {
	if head == "" {
		return tail
	}
	if tail == "" {
		return head
	}
	head = strings.TrimSuffix(head, "/")
	tail = strings.TrimPrefix(tail, "/")
	return head + "/" + tail
}
