package interceptor

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"unicode"
)

const (
	requestLogRedactedValue        = "***"
	maxRequestLogEmbeddedJSONBytes = 256 << 10
	maxRequestLogEmbeddedJSONDepth = 4
)

type replayReadCloser struct {
	io.Reader
	closer io.Closer
}

func (r *replayReadCloser) Close() error {
	return r.closer.Close()
}

func captureRequestBodyForLog(body io.ReadCloser) ([]byte, io.ReadCloser) {
	if body == nil {
		return nil, nil
	}
	prefix, _ := io.ReadAll(io.LimitReader(body, maxRequestLogEmbeddedJSONBytes+1))
	replayed := &replayReadCloser{
		Reader: io.MultiReader(bytes.NewReader(prefix), body),
		closer: body,
	}
	return prefix, replayed
}

func redactRequestBodyForLog(body []byte) string {
	if len(body) == 0 {
		return ""
	}
	if len(body) > maxRequestLogEmbeddedJSONBytes {
		return requestLogRedactedValue
	}

	value, err := decodeRequestLogJSON(string(body))
	if err != nil {
		return fmt.Sprintf("<non-JSON body omitted; size=%d>", len(body))
	}
	redactRequestLogValue(value, 0)
	redacted, err := json.Marshal(value)
	if err != nil {
		return fmt.Sprintf("<unserializable body omitted; size=%d>", len(body))
	}
	return string(redacted)
}

func redactQueryForLog(query url.Values) url.Values {
	redacted := make(url.Values, len(query))
	for key, values := range query {
		if isSensitiveRequestLogField(key) {
			redacted[key] = []string{requestLogRedactedValue}
			continue
		}
		redacted[key] = append([]string(nil), values...)
	}
	return redacted
}

func redactHeadersForLog(headers http.Header) http.Header {
	redacted := make(http.Header, len(headers))
	for key, values := range headers {
		if isSensitiveRequestLogField(key) {
			redacted[key] = []string{requestLogRedactedValue}
			continue
		}
		redacted[key] = append([]string(nil), values...)
	}
	return redacted
}

func decodeRequestLogJSON(input string) (any, error) {
	decoder := json.NewDecoder(strings.NewReader(input))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return nil, err
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		if err == nil {
			return nil, fmt.Errorf("multiple JSON values")
		}
		return nil, err
	}
	return value, nil
}

func redactRequestLogValue(value any, embeddedDepth int) {
	switch concrete := value.(type) {
	case map[string]any:
		for key, item := range concrete {
			if isSensitiveRequestLogField(key) {
				concrete[key] = requestLogRedactedValue
				continue
			}
			if stringValue, ok := item.(string); ok {
				if isSensitiveRequestLogURIField(key) {
					concrete[key] = redactRequestLogURL(stringValue)
					continue
				}
				if isRequestLogJSONCarrier(key) && looksLikeRequestLogJSON(stringValue) {
					concrete[key] = redactRequestLogEmbeddedJSON(stringValue, embeddedDepth)
					continue
				}
			}
			redactRequestLogValue(item, embeddedDepth)
		}
	case []any:
		for _, item := range concrete {
			redactRequestLogValue(item, embeddedDepth)
		}
	}
}

func redactRequestLogEmbeddedJSON(input string, embeddedDepth int) string {
	if len(input) > maxRequestLogEmbeddedJSONBytes || embeddedDepth >= maxRequestLogEmbeddedJSONDepth {
		return requestLogRedactedValue
	}
	value, err := decodeRequestLogJSON(input)
	if err != nil {
		return requestLogRedactedValue
	}
	redactRequestLogValue(value, embeddedDepth+1)
	redacted, err := json.Marshal(value)
	if err != nil {
		return requestLogRedactedValue
	}
	return string(redacted)
}

func looksLikeRequestLogJSON(value string) bool {
	trimmed := strings.TrimSpace(value)
	return strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[")
}

func isRequestLogJSONCarrier(key string) bool {
	switch normalizeRequestLogField(key) {
	case "actionargstring", "payloadjson", "requestjson", "responsejson":
		return true
	default:
		return false
	}
}

func isSensitiveRequestLogURIField(key string) bool {
	return normalizeRequestLogField(key) == "sinkuri"
}

func redactRequestLogURL(rawURL string) string {
	if rawURL == "" {
		return rawURL
	}
	parsed, err := url.Parse(rawURL)
	if err != nil {
		return fmt.Sprintf("<invalid URL omitted; size=%d>", len(rawURL))
	}
	if parsed.User != nil {
		parsed.User = url.UserPassword(requestLogRedactedValue, requestLogRedactedValue)
	}
	query := parsed.Query()
	for key := range query {
		if isSensitiveRequestLogField(key) {
			query[key] = []string{requestLogRedactedValue}
		}
	}
	parsed.RawQuery = query.Encode()
	if parsed.Fragment != "" {
		parsed.Fragment = requestLogRedactedValue
	}
	return parsed.String()
}

func isSensitiveRequestLogField(key string) bool {
	normalized := normalizeRequestLogField(key)
	if normalized == "ca" || normalized == "sig" || normalized == "signature" || normalized == "sas" {
		return true
	}
	if normalized == "cloudroleidentity" {
		return true
	}
	for _, marker := range []string{
		"password", "passwd", "credential", "token", "secret", "secrt",
		"accesskey", "apikey", "privatekey", "serviceaccountkey", "authorization", "signature", "cookie",
	} {
		if strings.Contains(normalized, marker) {
			return true
		}
	}
	return false
}

func normalizeRequestLogField(key string) string {
	return strings.Map(func(r rune) rune {
		if unicode.IsLetter(r) || unicode.IsDigit(r) {
			return unicode.ToLower(r)
		}
		return -1
	}, key)
}
