package audit

import (
	"encoding/json"
	"net/url"
	"strings"
	"unicode/utf8"
)

// Sanitizer handles redaction of sensitive data in audit logs.
type Sanitizer struct {
	sensitiveQueryParams map[string]struct{}
	sensitiveBodyFields  map[string]struct{}
}

// NewSanitizer creates a new Sanitizer with default sensitive fields
// and additional fields from configuration.
func NewSanitizer(additionalQueryParams, additionalBodyFields []string) *Sanitizer {
	s := &Sanitizer{
		sensitiveQueryParams: make(map[string]struct{}),
		sensitiveBodyFields:  make(map[string]struct{}),
	}

	// Add default sensitive query params
	for k := range defaultSensitiveQueryParams {
		s.sensitiveQueryParams[k] = struct{}{}
	}

	// Add configured additional query params (case-insensitive)
	for _, p := range additionalQueryParams {
		s.sensitiveQueryParams[strings.ToLower(p)] = struct{}{}
	}

	// Add default sensitive body fields
	for k := range baseSensitiveFields {
		s.sensitiveBodyFields[k] = struct{}{}
	}

	// Add configured additional body fields (case-insensitive)
	for _, f := range additionalBodyFields {
		s.sensitiveBodyFields[strings.ToLower(f)] = struct{}{}
	}

	return s
}

// SanitizePath redacts sensitive query parameters from the URL path.
func (s *Sanitizer) SanitizePath(rawPath string) string {
	// Parse the path to extract query string
	parsedURL, err := url.Parse(rawPath)
	if err != nil {
		// If parsing fails, avoid returning the original path to prevent leaking sensitive data
		return "[INVALID_URL]"
	}

	query := parsedURL.Query()
	if len(query) == 0 {
		return rawPath
	}

	// Check if any sensitive params exist
	modified := false
	for param := range query {
		lowerParam := strings.ToLower(param)
		if _, ok := s.sensitiveQueryParams[lowerParam]; ok {
			query.Set(param, redactedValue)
			modified = true
		}
	}

	if !modified {
		return rawPath
	}

	parsedURL.RawQuery = query.Encode()
	return parsedURL.String()
}

// SanitizeBody sanitizes request body by redacting sensitive JSON fields
// and truncating to maxSize if necessary.
// If alreadyTruncated is true, truncation suffix is added regardless of current size.
func (s *Sanitizer) SanitizeBody(body []byte, maxSize int, alreadyTruncated bool) string {
	if len(body) == 0 {
		return ""
	}

	// Try to parse as JSON and redact sensitive fields
	var data interface{}
	if err := json.Unmarshal(body, &data); err != nil {
		// Not valid JSON, treat as plain text
		result := string(body)
		if len(result) > maxSize {
			return truncateUTF8(result, maxSize) + truncatedSuffix
		}
		// Body was already truncated during capture
		if alreadyTruncated {
			return result + truncatedSuffix
		}
		return result
	}

	// Recursively redact sensitive fields
	s.redactSensitiveFields(data, 0)

	// Marshal back to JSON
	sanitized, err := json.Marshal(data)
	if err != nil {
		// Should not happen, but fallback to truncated original
		result := string(body)
		if len(result) > maxSize {
			return truncateUTF8(result, maxSize) + truncatedSuffix
		}
		if alreadyTruncated {
			return result + truncatedSuffix
		}
		return result
	}

	result := string(sanitized)
	if len(result) > maxSize {
		return truncateUTF8(result, maxSize) + truncatedSuffix
	}
	// Body was already truncated during capture
	if alreadyTruncated {
		return result + truncatedSuffix
	}
	return result
}

// redactSensitiveFields recursively traverses JSON data and redacts sensitive fields.
// depth parameter limits recursion to prevent stack overflow.
// When max depth is exceeded, entire subtree is redacted to prevent leaking deeply nested sensitive data.
func (s *Sanitizer) redactSensitiveFields(data interface{}, depth int) {
	if depth > maxRecursionDepth {
		// At or beyond max depth, redact entire remaining subtree to avoid leaking
		// deeply nested sensitive data while still preventing excessive recursion.
		switch v := data.(type) {
		case map[string]interface{}:
			for key := range v {
				v[key] = redactedValue
			}
		case []interface{}:
			for i := range v {
				v[i] = redactedValue
			}
		}
		return
	}

	switch v := data.(type) {
	case map[string]interface{}:
		for key, value := range v {
			lowerKey := strings.ToLower(key)
			if _, ok := s.sensitiveBodyFields[lowerKey]; ok {
				// Redact the value
				v[key] = redactedValue
			} else {
				// Recursively process nested structures
				s.redactSensitiveFields(value, depth+1)
			}
		}
	case []interface{}:
		for _, item := range v {
			s.redactSensitiveFields(item, depth+1)
		}
	}
}

// SanitizeQueryParams redacts sensitive query parameters from a raw query string.
// Unlike SanitizePath, this method only handles the query string portion (without the path).
// Returns query params in consistent alphabetical order for log analysis.
func (s *Sanitizer) SanitizeQueryParams(rawQuery string) string {
	if rawQuery == "" {
		return ""
	}

	query, err := url.ParseQuery(rawQuery)
	if err != nil {
		return "[INVALID_QUERY]"
	}

	// Check and redact sensitive params
	for param := range query {
		lowerParam := strings.ToLower(param)
		if _, ok := s.sensitiveQueryParams[lowerParam]; ok {
			query.Set(param, redactedValue)
		}
	}

	// Always use Encode() for consistent ordering in logs
	return query.Encode()
}

// truncateUTF8 truncates a string to maxLen bytes without breaking UTF-8 characters.
func truncateUTF8(s string, maxLen int) string {
	if maxLen <= 0 {
		return ""
	}
	if len(s) <= maxLen {
		return s
	}

	// range iterates by rune; i is the byte index where each rune starts
	lastValidEnd := 0
	for i, r := range s {
		end := i + utf8.RuneLen(r)
		if end > maxLen {
			break
		}
		lastValidEnd = end
	}
	return s[:lastValidEnd]
}
