// Package audit — content-type-aware body scrub dispatch for the request
// capture middleware. Extracted from request_capture_middleware.go to keep
// both files under the 400-line budget and to localize all §14.3 body-scrub
// coverage decisions in one place.
package audit

import (
	"encoding/json"
	"strings"
)

// isScrubbableContentType reports whether the content-type is covered by a
// known scrubber (json or form-urlencoded). Used by strict mode to decide
// whether to drop a capture row vs pass-through the body.
//
// Empty CT on bodyless requests (GET / HEAD / DELETE without body) is
// treated as scrubbable — there is no body to leak.
func isScrubbableContentType(contentType string) bool {
	ct := strings.ToLower(contentType)
	if ct == "" {
		return true
	}
	if strings.Contains(ct, "json") {
		return true
	}
	if strings.Contains(ct, "application/x-www-form-urlencoded") {
		return true
	}
	return false
}

// scrubBodyByContentType dispatches §14.3 body scrubbing based on
// Content-Type:
//   - `application/json` (or any CT containing "json") → ScrubJSONPayload.
//   - `application/x-www-form-urlencoded` → ScrubFormBody (same blacklist).
//   - XML / text/plain / multipart → pass-through, bounded by the upstream
//     size cap. // TODO(Phase 2B): scrub XML/multipart.
//
// On parse failure for a recognized CT (malformed JSON or form body), we
// fall through to the raw bytes rather than dropping the row — strict mode
// (§14.11) is the hammer for "never write PII-risk rows"; here we preserve
// observability.
func scrubBodyByContentType(contentType string, body []byte) string {
	if len(body) == 0 {
		return ""
	}
	ct := strings.ToLower(contentType)
	if strings.Contains(ct, "json") {
		if scrubbed, err := ScrubJSONPayload(body); err == nil {
			return string(scrubbed)
		}
		return string(body)
	}
	if strings.Contains(ct, "application/x-www-form-urlencoded") {
		if scrubbed, err := ScrubFormBody(body); err == nil {
			return string(scrubbed)
		}
		return string(body)
	}
	// TODO(Phase 2B): scrub XML/multipart — for now, pass-through within cap.
	return string(body)
}

// scrubSecretValueBodyFailClosed is for secretValue write routes. It preserves
// only the exact accepted JSON schema with
// `secretValue` redacted; malformed or schema-invalid JSON falls back to the
// sentinel so validation errors cannot persist attempted secrets.
func scrubSecretValueBodyFailClosed(body []byte) string {
	if len(body) == 0 {
		return PIIScrubSentinel
	}
	var obj map[string]json.RawMessage
	if err := json.Unmarshal(body, &obj); err != nil {
		return PIIScrubSentinel
	}
	raw, ok := obj["secretValue"]
	if !ok || len(obj) != 1 {
		return PIIScrubSentinel
	}
	var secretValue *string
	if err := json.Unmarshal(raw, &secretValue); err != nil {
		return PIIScrubSentinel
	}
	if secretValue == nil {
		return PIIScrubSentinel
	}
	if strings.TrimSpace(*secretValue) == "" {
		return PIIScrubSentinel
	}
	return `{"secretValue":"` + PIIScrubSentinel + `"}`
}

// isSecretValueWritePath identifies current admin routes whose request body
// carries write-only credential material before handler-level validation runs.
func isSecretValueWritePath(method, path string) bool {
	if !strings.EqualFold(method, "POST") {
		return false
	}
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) == 5 &&
		parts[0] == "api" &&
		parts[1] == "named-credentials" &&
		parts[2] != "" &&
		parts[3] == "secrets" &&
		parts[4] != "" {
		return true
	}
	return len(parts) == 7 &&
		parts[0] == "services" &&
		parts[1] == "data" &&
		parts[2] != "" &&
		parts[3] == "connector-webhooks" &&
		parts[4] == "secrets" &&
		parts[5] != "" &&
		parts[6] == "rotate"
}
