// Copyright 2024 The Prometheus 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.

// This package no longer handles safe yaml parsing. In order to
// ensure correct yaml unmarshalling, use "yaml.UnmarshalStrict()".

package httpconfig

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
)

// reservedHeaders that change the connection, are set by Prometheus, or can
// be changed otherwise.
var reservedHeaders = map[string]struct{}{
	"Authorization":                       {},
	"Host":                                {},
	"Content-Encoding":                    {},
	"Content-Length":                      {},
	"Content-Type":                        {},
	"User-Agent":                          {},
	"Connection":                          {},
	"Keep-Alive":                          {},
	"Proxy-Authenticate":                  {},
	"Proxy-Authorization":                 {},
	"Www-Authenticate":                    {},
	"Accept-Encoding":                     {},
	"X-Prometheus-Remote-Write-Version":   {},
	"X-Prometheus-Remote-Read-Version":    {},
	"X-Prometheus-Scrape-Timeout-Seconds": {},

	// Added by SigV4.
	"X-Amz-Date":           {},
	"X-Amz-Security-Token": {},
	"X-Amz-Content-Sha256": {},
}

// Headers represents the configuration for HTTP headers.
type Headers struct {
	Headers map[string]Header `yaml:",inline"`
	dir     string
}

// Header represents the configuration for a single HTTP header.
type Header struct {
	Values  []string `yaml:"values,omitempty" json:"values,omitempty"`
	Secrets []Secret `yaml:"secrets,omitempty" json:"secrets,omitempty"`
	Files   []string `yaml:"files,omitempty" json:"files,omitempty"`
}

func (h Headers) MarshalJSON() ([]byte, error) {
	// Inline the Headers map when serializing JSON because json encoder doesn't support "inline" directive.
	return json.Marshal(h.Headers)
}

func (h *Headers) UnmarshalJSON(data []byte) error {
	// Inline the Headers map when serializing JSON because json encoder doesn't support "inline" directive.
	return json.Unmarshal(data, &h.Headers)
}

// SetDirectory records the directory to make headers file relative to the
// configuration file.
func (h *Headers) SetDirectory(dir string) {
	if h == nil {
		return
	}
	h.dir = dir
}

// Validate validates the Headers config.
func (h *Headers) Validate() error {
	for n, header := range h.Headers {
		if _, ok := reservedHeaders[http.CanonicalHeaderKey(n)]; ok {
			return fmt.Errorf("setting header %q is not allowed", http.CanonicalHeaderKey(n))
		}
		for _, v := range header.Files {
			f := JoinDir(h.dir, v)
			_, err := os.ReadFile(f)
			if err != nil {
				return fmt.Errorf("unable to read header %q from file %s: %w", http.CanonicalHeaderKey(n), f, err)
			}
		}
	}
	return nil
}

// NewHeadersRoundTripper returns a RoundTripper that sets HTTP headers on
// requests as configured.
func NewHeadersRoundTripper(config *Headers, next http.RoundTripper) http.RoundTripper {
	if len(config.Headers) == 0 {
		return next
	}
	return &headersRoundTripper{
		config: config,
		next:   next,
	}
}

type headersRoundTripper struct {
	next   http.RoundTripper
	config *Headers
}

// RoundTrip implements http.RoundTripper.
func (rt *headersRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	req = cloneRequest(req)
	for n, h := range rt.config.Headers {
		for _, v := range h.Values {
			req.Header.Add(n, v)
		}
		for _, v := range h.Secrets {
			req.Header.Add(n, string(v))
		}
		for _, v := range h.Files {
			f := JoinDir(rt.config.dir, v)
			b, err := os.ReadFile(f)
			if err != nil {
				return nil, fmt.Errorf("unable to read headers file %s: %w", f, err)
			}
			req.Header.Add(n, strings.TrimSpace(string(b)))
		}
	}
	return rt.next.RoundTrip(req)
}

// CloseIdleConnections implements closeIdler.
func (rt *headersRoundTripper) CloseIdleConnections() {
	if ci, ok := rt.next.(closeIdler); ok {
		ci.CloseIdleConnections()
	}
}
