package git_pages

import (
	"errors"
	"fmt"
	"net/http"
	"net/textproto"
	"net/url"
	"slices"
	"strings"

	"codeberg.org/git-pages/go-headers"
	"google.golang.org/protobuf/proto"
)

var ErrHeaderNotAllowed = errors.New("custom header not allowed")

const HeadersFileName string = "_headers"

// Lifted from https://docs.netlify.com/manage/routing/headers/, except for `Set-Cookie`
// the rationale for which does not apply in our environment.
var unsafeHeaders []string = []string{
	"Accept-Ranges",
	"Age",
	"Allow",
	"Alt-Svc",
	"Connection",
	"Content-Encoding",
	"Content-Length",
	"Content-Range",
	"Date",
	"Location", // use `_redirects` instead
	"Server",
	"Trailer",
	"Transfer-Encoding",
	"Upgrade",
}

func IsAllowedCustomHeader(header string) bool {
	header = textproto.CanonicalMIMEHeaderKey(header)
	switch {
	case slices.Contains(unsafeHeaders, header):
		return false // explicitly unsafe
	case slices.Contains(config.Limits.AllowedCustomHeaders, header):
		return true // explicitly allowlisted
	default:
		return false // deny by default; we don't know what the future holds
	}
}

func validateHeaderRule(rule headers.Rule) error {
	url, err := url.Parse(rule.Path)
	if err != nil {
		return fmt.Errorf("malformed path")
	}
	if url.Scheme != "" {
		return fmt.Errorf("path must not contain a scheme")
	}
	if !strings.HasPrefix(url.Path, "/") {
		return fmt.Errorf("path must start with a /")
	}
	// Per Netlify documentation:
	// > Wildcards (*) can be used at any place inside of a path segment to match any character.
	// However, we currently do not implement this, for simplicity. Instead we implement a strict
	// subset of the syntactically allowed wildcards.
	if strings.Contains(url.Path, "*") && !strings.HasSuffix(url.Path, "/*") {
		return fmt.Errorf("splat * must be its own final segment of the path")
	}
	// Note that this isn't our only line of defense against forbidden headers;
	// the purpose of this check is just to inform the uploader of a problem.
	// If the validation rules change after a manifest is uploaded, we could
	// still end up attempting to serve a forbidden header.
	for header := range rule.Headers {
		if slices.Contains(unsafeHeaders, header) {
			return fmt.Errorf("rule sets header %q (fundamentally unsafe)", header)
		}
		if !slices.Contains(config.Limits.AllowedCustomHeaders, header) {
			return fmt.Errorf("rule sets header %q (not allowlisted)", header)
		}
		if !IsAllowedCustomHeader(header) { // make sure we don't desync
			panic(errors.New("header check inconsistency"))
		}
	}
	return nil
}

// Parses redirects file and injects rules into the manifest.
func ProcessHeadersFile(manifest *Manifest) error {
	headersEntry := manifest.Contents[HeadersFileName]
	delete(manifest.Contents, HeadersFileName)
	if headersEntry == nil {
		return nil
	} else if headersEntry.GetType() != Type_InlineFile {
		return AddProblem(manifest, HeadersFileName,
			"not a regular file")
	}

	rules, err := headers.ParseString(string(headersEntry.GetData()))
	if err != nil {
		return AddProblem(manifest, HeadersFileName,
			"syntax error: %s", err)
	}

	for index, rule := range rules {
		if err := validateHeaderRule(rule); err != nil {
			AddProblem(manifest, HeadersFileName,
				"rule #%d %q: %s", index+1, rule.Path, err)
			continue
		}
		headerMap := []*Header{}
		for header, values := range rule.Headers {
			headerMap = append(headerMap, &Header{
				Name:   proto.String(header),
				Values: values,
			})
		}
		manifest.Headers = append(manifest.Headers, &HeaderRule{
			Path:      proto.String(rule.Path),
			HeaderMap: headerMap,
		})
	}
	return nil
}

func CollectHeadersFile(manifest *Manifest) string {
	var headersRules []headers.Rule
	for _, manifestRule := range manifest.GetHeaders() {
		headersRule := headers.Rule{
			Path:    manifestRule.GetPath(),
			Headers: http.Header{},
		}
		for _, manifestHeader := range manifestRule.GetHeaderMap() {
			headersRule.Headers[manifestHeader.GetName()] = manifestHeader.GetValues()
		}
		headersRules = append(headersRules, headersRule)
	}
	return headers.Must(headers.UnparseString(headersRules))
}

func ApplyHeaderRules(manifest *Manifest, url *url.URL) (headers http.Header, err error) {
	headers = http.Header{}
	fromSegments := pathSegments(url.Path)
next:
	for _, rule := range manifest.Headers {
		// check if the rule matches url
		ruleURL, _ := url.Parse(*rule.Path) // pre-validated in `validateHeaderRule`
		ruleSegments := pathSegments(ruleURL.Path)
		if ruleSegments[len(ruleSegments)-1] != "*" {
			if len(ruleSegments) < len(fromSegments) {
				continue
			}
		}
		for index, ruleFromSegment := range ruleSegments {
			if ruleFromSegment == "*" {
				break
			}
			if len(fromSegments) <= index {
				continue next
			}
			if fromSegments[index] != ruleFromSegment {
				continue next
			}
		}
		// the rule has matched url, validate headers against up-to-date policy
		for _, header := range rule.HeaderMap {
			name := header.GetName()
			if !IsAllowedCustomHeader(name) {
				return nil, fmt.Errorf("%w: %s", ErrHeaderNotAllowed, name)
			}
			for _, value := range header.GetValues() {
				headers.Add(name, value)
			}
		}
		break
	}
	return
}
