package utils

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

const (
	redactedValue            = "***"
	maxEmbeddedJSONBytes     = 256 << 10
	maxEmbeddedJSONDepth     = 4
	nonJSONPayloadLogMessage = "<non-JSON payload omitted; size=%d>"
)

// RedactSecrets redacts credentials in a JSON log payload. In addition to
// ordinary JSON objects and arrays, it handles the explicitly supported JSON
// carrier fields (for example action_arg_string) without attempting to parse
// every string in the payload.
func RedactSecrets(msgJSON string) string {
	if msgJSON == "" {
		return msgJSON
	}

	value, err := decodeJSON(msgJSON)
	if err != nil {
		return fmt.Sprintf(nonJSONPayloadLogMessage, len(msgJSON))
	}

	redactJSONValue(value, 0)
	redacted, err := json.Marshal(value)
	if err != nil {
		return fmt.Sprintf(nonJSONPayloadLogMessage, len(msgJSON))
	}
	return string(redacted)
}

// RedactedJSON marshals an arbitrary value and returns a log-safe JSON string.
// It never falls back to formatting the original value when marshaling fails.
func RedactedJSON(value any) string {
	switch concrete := value.(type) {
	case string:
		if looksLikeJSON(concrete) {
			return RedactSecrets(concrete)
		}
		return fmt.Sprintf("<string payload omitted; size=%d>", len(concrete))
	case []byte:
		if looksLikeJSON(string(concrete)) {
			return RedactSecrets(string(concrete))
		}
		return fmt.Sprintf("<byte payload omitted; size=%d>", len(concrete))
	}

	data, err := json.Marshal(value)
	if err != nil {
		return fmt.Sprintf("<unserializable payload omitted; type=%T>", value)
	}
	return RedactSecrets(string(data))
}

// RedactURL removes credentials from a URL while preserving routing information
// such as the scheme, host, path, and non-sensitive query parameters.
func RedactURL(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(redactedValue, redactedValue)
	}
	query := parsed.Query()
	for key := range query {
		if isSensitiveLogField(key) {
			query[key] = []string{redactedValue}
		}
	}
	parsed.RawQuery = query.Encode()
	if parsed.Fragment != "" {
		parsed.Fragment = redactedValue
	}
	return parsed.String()
}

func decodeJSON(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
	}
	return value, nil
}

func redactJSONValue(value any, embeddedDepth int) {
	switch concrete := value.(type) {
	case map[string]any:
		redactJSONMap(concrete, embeddedDepth)
	case []any:
		for _, item := range concrete {
			redactJSONValue(item, embeddedDepth)
		}
	}
}

func redactJSONMap(value map[string]any, embeddedDepth int) {
	for key, item := range value {
		if isSensitiveLogField(key) {
			value[key] = redactedValue
			continue
		}

		if stringValue, ok := item.(string); ok {
			if isSensitiveLogURIField(key) {
				value[key] = RedactURL(stringValue)
				continue
			}
			if isEmbeddedJSONField(key) && looksLikeJSON(stringValue) {
				value[key] = redactEmbeddedJSON(stringValue, embeddedDepth)
				continue
			}
		}
		redactJSONValue(item, embeddedDepth)
	}
}

func redactEmbeddedJSON(input string, embeddedDepth int) string {
	if len(input) > maxEmbeddedJSONBytes || embeddedDepth >= maxEmbeddedJSONDepth {
		return redactedValue
	}

	value, err := decodeJSON(input)
	if err != nil {
		// This was an explicitly supported JSON carrier and looked like JSON.
		// Fail closed rather than logging a malformed credential payload.
		return redactedValue
	}
	redactJSONValue(value, embeddedDepth+1)
	redacted, err := json.Marshal(value)
	if err != nil {
		return redactedValue
	}
	return string(redacted)
}

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

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

func isSensitiveLogURIField(key string) bool {
	return normalizeLogField(key) == "sinkuri"
}

func isSensitiveLogField(key string) bool {
	normalized := normalizeLogField(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 normalizeLogField(key string) string {
	return strings.Map(func(r rune) rune {
		if unicode.IsLetter(r) || unicode.IsDigit(r) {
			return unicode.ToLower(r)
		}
		return -1
	}, key)
}

// Kept for focused unit tests and callers that already operate on decoded JSON.
func dfsRedactMap(value map[string]any) {
	redactJSONMap(value, 0)
}

// Kept for focused unit tests and callers that already operate on decoded JSON.
func dfsRedactSlice(value []any) {
	redactJSONValue(value, 0)
}
