package redact

import (
	"bytes"
	"encoding/json"
	"net/url"
	"regexp"
	"strings"
)

const redactedText = "[REDACTED]"

var (
	quotedURIInText = regexp.MustCompile(`'[a-zA-Z][a-zA-Z0-9+.-]*://[^']*'`)
	// Include backslashes so JSON-escaped query separators do not split a URI.
	uriInText        = regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9+.-]*://[^\s"'<>]+`)
	privateKeyInText = regexp.MustCompile(`(?s)-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----.*?(?:-----END (?:[A-Z]+ )*PRIVATE KEY-----|$)`)
)

// URI prepares a URI for diagnostics, without modifying the URI used to connect.
// Query strings and fragments are opaque: credentials need not use known names.
// Invalid URIs are omitted rather than risking disclosure in a parsing error.
func URI(raw string) string {
	u, err := url.Parse(raw)
	if err != nil {
		return redactedText
	}
	if u.User != nil {
		u.User = url.User(redactedText)
	}
	if u.RawQuery != "" || u.ForceQuery {
		u.RawQuery = redactedText
		u.ForceQuery = false
	}
	if u.Fragment != "" {
		u.Fragment = redactedText
		u.RawFragment = ""
	}
	return u.String()
}

// Text removes credentials from downstream diagnostic messages before logging,
// persisting or returning them. It also handles JSON error envelopes and nested
// JSON strings. Apply it before truncating a message, never to connection inputs.
func Text(message string) string {
	return redactText(message, 0)
}

func redactText(message string, depth int) string {
	if depth > 16 {
		return redactedText
	}
	message = privateKeyInText.ReplaceAllString(message, redactedText)
	trimmed := strings.TrimSpace(message)
	arrayTail := strings.TrimSpace(strings.TrimPrefix(trimmed, "["))
	jsonArray := strings.HasPrefix(trimmed, "[") && (json.Valid([]byte(message)) || strings.HasPrefix(arrayTail, "{") || strings.HasPrefix(arrayTail, `"`))
	// Bracketed error codes and source locations are not JSON arrays.
	if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, `"`) || jsonArray {
		if !json.Valid([]byte(message)) {
			// A truncated envelope cannot be reliably traversed for secret fields.
			return redactedText
		}
		var value interface{}
		decoder := json.NewDecoder(bytes.NewBufferString(message))
		decoder.UseNumber()
		if decoder.Decode(&value) != nil {
			return redactedText
		}
		value = redactJSONValue(value, depth+1)
		result, err := json.Marshal(value)
		if err != nil {
			return redactedText
		}
		return string(result)
	}
	// Sink errors single-quote their URI, including any invalid whitespace.
	// Double quotes can enclose a whole JSON error string, including its cause.
	message = quotedURIInText.ReplaceAllStringFunc(message, func(quoted string) string {
		return quoted[:1] + URI(quoted[1:len(quoted)-1]) + quoted[len(quoted)-1:]
	})
	return uriInText.ReplaceAllStringFunc(message, URI)
}

func redactJSONValue(value interface{}, depth int) interface{} {
	if depth > 16 {
		return redactedText
	}
	switch v := value.(type) {
	case string:
		return redactText(v, depth+1)
	case map[string]interface{}:
		for key, child := range v {
			normalized := strings.NewReplacer("-", "", "_", "").Replace(strings.ToLower(key))
			sensitive := normalized == "key" || strings.Contains(normalized, "privatekey") || strings.Contains(normalized, "sslkey")
			for _, field := range PasswordFields {
				field = strings.NewReplacer("-", "", "_", "").Replace(field)
				sensitive = sensitive || strings.Contains(normalized, field)
			}
			if sensitive {
				v[key] = redactedText
			} else {
				v[key] = redactJSONValue(child, depth+1)
			}
		}
	case []interface{}:
		for i := range v {
			v[i] = redactJSONValue(v[i], depth+1)
		}
	}
	return value
}
