package signature

import (
	"bytes"
	"net/http"
	"net/textproto"
	"sort"
	"strings"

	"code.justin.tv/sse/malachai/pkg/internal/stringutil"
)

var blacklistedHeaders = map[string]interface{}{
	"x-forwarded-for":   nil,
	"x-forwarded-proto": nil,
	"x-forwarded-port":  nil,
	"x-amzn-trace-id":   nil,
}

func newCanonicalHeader(h http.Header) *canonicalHeader {
	return &canonicalHeader{Header: h}
}

// canonicalHeader provides a way to marshal a header into the canonical version
// to be signed (or validated) via S2S.
type canonicalHeader struct {
	http.Header

	// onlyHeaders limits what headers we attempt to serialize.
	// If it is nil, we will marshal all headers.
	onlyHeaders *[]string
}

func (ch *canonicalHeader) OnlyHeaders(h []string) *canonicalHeader {
	ch.onlyHeaders = &h
	return ch
}

func (ch canonicalHeader) Marshal() ([]byte, error) {
	b := bytes.NewBuffer(nil)
	for iH, h := range ch.kvItems() {
		if iH > 0 {
			if _, err := b.WriteString("\n"); err != nil {
				return nil, err
			}
		}
		if _, err := b.WriteString(strings.ToLower(h.Key)); err != nil {
			return nil, err
		}
		if _, err := b.WriteString(": "); err != nil {
			return nil, err
		}
		if _, err := b.WriteString(h.Value); err != nil {
			return nil, err
		}
	}
	return b.Bytes(), nil
}

// kvItems returns a list of kvItem sorted by ch.canonicalHeaderLexographicKey
func (ch canonicalHeader) kvItems() kvItems {
	// as an estimate, allocate cap for the number of keys. there may be more or
	// less but this is usually about right.
	items := make(kvItems, 0, 2*len(ch.Header))
	ch.iterHeaders(
		func(key string, values []string) {
			if _, ok := blacklistedHeaders[strings.ToLower(key)]; ok {
				return
			}

			for _, value := range values {
				items = append(items, kvItem{
					Key:   key,
					Value: value,
				})
			}
		},
		ch.getFilter(),
	)
	sort.Sort(items)
	return items
}

// sort headers by this value
func (ch canonicalHeader) canonicalHeaderLexographicKey(k string) string {
	return textproto.CanonicalMIMEHeaderKey(k)
}

func (ch canonicalHeader) getFilter() func(string) bool {
	if ch.onlyHeaders != nil {
		acceptedKeys := stringutil.NewSet()
		for _, key := range *ch.onlyHeaders {
			acceptedKeys.Add(ch.canonicalHeaderLexographicKey(key))
		}
		return acceptedKeys.Contains
	}

	return func(string) bool {
		return true
	}
}

func (ch canonicalHeader) iterHeaders(
	handler func(string, []string),
	filter func(string) bool,
) {
	for key, values := range ch.Header {
		key = ch.canonicalHeaderLexographicKey(key)
		if filter(key) {
			handler(key, values)
		}
	}
}
