package run

import (
	"bytes"
	"encoding/json"
	"math"
	"sort"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/workflow/eventcontract"
	runkernel "github.com/tidbcloud/AgentTeam/internal/workflow/run"
)

func cloneJSON(value json.RawMessage) json.RawMessage {
	if len(value) == 0 {
		return nil
	}
	return append(json.RawMessage(nil), value...)
}

// safeOperationalJSON removes infrastructure-only keys recursively from
// effect receipts, event payloads, and operational errors. User-authored Run
// input/state/output are not passed through this filter, so legitimate Plan
// field names are preserved exactly.
//
// Legacy error objects persisted as {code, message} (before the public runtime
// failure contract) are projected: the arbitrary authoring `message` is never
// projected — a known code maps to the bounded safe problem/hint via the stable
// runtime mapping, and unknown prose fails closed to a generic bounded problem.
// Structured public fields (stage/path/problem/hint/retryable/diagnostics/
// workItem) are retained.
func safeOperationalJSON(payload json.RawMessage) json.RawMessage {
	if len(bytes.TrimSpace(payload)) == 0 {
		return nil
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return nil
	}
	// The operational scrubber only accepts a flat object (an error receipt or
	// request envelope). A scalar, array, or null top-level value fails closed to
	// nil so no unbounded/operational scalar can reach the public DTO.
	if _, ok := value.(map[string]any); !ok {
		return nil
	}
	scrubOperationalValue(value)
	projectLegacyErrorObject(value)
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil
	}
	return encoded
}

// safeOperationalEventJSON projects a durable Run event payload through the
// schema-known per-event envelope projection. Event envelopes carry reviewed
// bounded activation/node/scope/effect/kind/retry/message/receipt/recovery
// context alongside an optional embedded failure contract. The whole-object
// failure replacement is NOT applied: instead the reviewed bounded envelope
// context is preserved through a typed allowlist, and ONLY the failure contract
// (code/stage/path/problem/hint/retryable/workItem/diagnostics/truncation) is
// projected through the shared typed allowlisted redaction/budget logic
// (projectFailureContract). Operational keys are stripped recursively before
// the schema-known projection, and unknown or secret-bearing envelope fields
// fail closed (dropped).
//
// Legacy error objects persisted as {code, message} inside an event envelope
// are projected by the shared projector, so the arbitrary authoring
// `message` is never projected.
func safeOperationalEventJSON(payload json.RawMessage, eventKind string) json.RawMessage {
	if len(bytes.TrimSpace(payload)) == 0 {
		return nil
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return nil
	}
	// The typed event projector only accepts a flat object envelope. A scalar,
	// array, or null top-level value is not a trustworthy event envelope and
	// fails closed to nil (never marshaled back out unchanged).
	if _, ok := value.(map[string]any); !ok {
		return nil
	}
	scrubOperationalValue(value)
	projectOperationalEventPayload(value, eventKind)
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil
	}
	return encoded
}

// projectOperationalEventPayload routes one decoded event envelope
// object. Every event kind is a closed typed public schema: the reviewed bounded
// context fields survive via the per-kind typed allowlist, the failure contract
// (when the kind allows it and a `code` is present) is projected through the
// shared typed projector, a flat {failureCode, ...} EffectResult inside an event
// keeps the reviewed envelope context AND projects its entire flat failure
// namespace through the exact typed projector (never inheriting the EffectResult
// response's open `output` exemption and never retaining unknown failure-prefixed
// secrets), and every other key is dropped. Unknown event kinds fail closed to
// the scrubbed empty object.
func projectOperationalEventPayload(value any, eventKind string) {
	object, ok := value.(map[string]any)
	if !ok {
		return
	}
	// The event-type discriminator is an identity: it is matched against the
	// closed event-kind vocabulary byte-for-byte on the ORIGINAL persisted bytes.
	// A padded, invalid-UTF-8, case/punctuation/token-bearing, or unknown type is
	// NEVER trimmed or normalized into an accepted kind — it fails closed to the
	// scrubbed empty object (no field may project). The byte-exact producer-owned
	// validator is used (not eventKindSchema, which trims) so a padded kind can
	// never unlock a payload projection while its top-level discriminator already
	// failed closed.
	if _, ok := eventcontract.ValidEventKind(eventKind); !ok {
		for key := range object {
			delete(object, key)
		}
		return
	}
	schema := eventKindSchema(eventKind)
	_, hasFailureCode := object["failureCode"]
	_, hasCode := object["code"]
	if schema == nil {
		// An event kind with no reviewed closed schema fails closed: no field may
		// survive, so no secret, tenant id, or unknown context can project.
		for key := range object {
			delete(object, key)
		}
		return
	}
	if hasFailureCode {
		// A flat {failureCode, ...} EffectResult inside an event envelope: keep the
		// reviewed bounded envelope context (per the event kind's typed allowlist)
		// and project the ENTIRE flat failure namespace through the exact typed
		// projector so every `failure*` field is validated against the reviewed
		// closed contracts and unknown failure-prefixed or secret-bearing fields
		// (failureSecret, failureToken, corruption-controlled values) fail closed.
		// The raw failureMessage is always dropped (never projected), and the
		// event-envelope path never inherits the EffectResult response's open
		// `output` exemption.
		if !schema.failureAllowed {
			// A flat failure on a kind that never carries one fails closed.
			for key := range object {
				delete(object, key)
			}
			return
		}
		projected := map[string]any{}
		for key, extract := range schema.context {
			if raw, present := object[key]; present {
				if value, ok := extract(raw); ok {
					projected[key] = value
				}
			}
		}
		// Project the flat failure namespace in place (keeps reviewed failure
		// fields, drops unknown failure-prefixed and secret-bearing fields, and
		// never inherits the open output exemption).
		projectEffectResultFailureFields(object)
		// Merge the surviving reviewed failure fields with the envelope context.
		for key, value := range object {
			projected[key] = value
		}
		for key := range object {
			delete(object, key)
		}
		for key, value := range projected {
			object[key] = value
		}
		return
	}
	if hasCode && !schema.failureAllowed {
		// A failure contract on a kind that never carries one fails closed.
		for key := range object {
			delete(object, key)
		}
		return
	}
	projectOperationalEventEnvelope(object, schema)
}

// projectOperationalEventEnvelope rebuilds an event envelope against the
// per-kind closed schema: reviewed bounded envelope context fields survive via
// the typed allowlist, the failure contract is projected through the shared typed
// projector (only when the kind allows it), and every other key is dropped.
func projectOperationalEventEnvelope(object map[string]any, schema *eventEnvelopeSchema) {
	projected := map[string]any{}
	for key, extract := range schema.context {
		if raw, present := object[key]; present {
			if value, ok := extract(raw); ok {
				projected[key] = value
			}
		}
	}
	if schema.failureAllowed {
		if _, hasCode := object["code"]; hasCode {
			for key, value := range projectFailureContract(object, readFailureTruncation(object)) {
				projected[key] = value
			}
		}
	}
	for key := range object {
		delete(object, key)
	}
	for key, value := range projected {
		object[key] = value
	}
}

// Read-time bounds for the schema-known event envelope context fields. They
// mirror the reviewed producer shapes so a malformed or oversized legacy row
// cannot push unbounded or operational text into the Run log/event surface.
const (
	readEnvelopeIDRunes   = 256
	readEnvelopeKindRunes = 128
	readEnvelopeTimeRunes = 64
	readEnvelopeListCap   = 64
)

// envelopeFieldBool keeps a strict boolean envelope field; a non-bool fails
// closed.
func envelopeFieldBool(raw any) (any, bool) {
	value, ok := raw.(bool)
	if !ok {
		return nil, false
	}
	return value, true
}

// projectLegacyErrorObject rewrites a legacy {code, message} error object
// so the arbitrary authoring `message` is never projected. When the code is a
// known runtime failure code the bounded public problem/hint are derived from
// the stable mapping; when the prose is unknown the object fails closed to the
// generic public problem and the raw message is dropped. Structured safe
// fields (stage/path/problem/hint/retryable/diagnostics/workItem) already in the
// payload are preserved.
func projectLegacyErrorObject(value any) {
	object, ok := value.(map[string]any)
	if !ok {
		return
	}
	_, hasFailureCode := object["failureCode"]
	_, hasCode := object["code"]
	if hasCode && !hasFailureCode {
		// A failure-shaped object (legacy {code, message} ErrorJSON or an
		// already-public failure object): route through the unified typed
		// allowlisted projector so problem/hint derive from the reviewed code
		// map, closed contracts are validated, and the shared
		// diagnostic/envelope budget applies.
		projected := projectFailureContract(object, readFailureTruncation(object))
		for key := range object {
			delete(object, key)
		}
		for key, value := range projected {
			object[key] = value
		}
		return
	}
	if hasFailureCode {
		// Flat EffectResult failure: route the complete flat failure namespace
		// through the unified typed projector so every field is validated against
		// the reviewed closed contracts and the shared budget produces one
		// deterministic prior+fresh truncation report. The raw failureMessage is
		// always dropped (never projected).
		projectEffectResultValue(object)
		return
	}
}

// readFailureTruncation decodes a prior truncation report from a raw failure
// object. The typed report is passed to the shared budget so a replay/re-read
// preserves the earlier cuts (omittedCount/droppedCount/truncatedFields/
// serializedBytes/minimalFallback). Only closed public field names are
// accepted into TruncatedFields.
func readFailureTruncation(raw map[string]any) *runkernel.FailureTruncation {
	value, ok := raw["truncation"].(map[string]any)
	if !ok {
		return nil
	}
	truncation := &runkernel.FailureTruncation{}
	if truncated, ok := value["diagnosticsTruncated"].(bool); ok {
		truncation.DiagnosticsTruncated = truncated
	}
	if count, ok := readFailureInteger(value["omittedCount"]); ok && count > 0 {
		truncation.OmittedCount = int(count)
	}
	if dropped, ok := readFailureInteger(value["droppedCount"]); ok && dropped > 0 {
		truncation.DroppedCount = int(dropped)
	}
	if fields, ok := value["truncatedFields"].([]any); ok {
		seen := make(map[string]struct{}, len(fields))
		for _, field := range fields {
			text, ok := field.(string)
			if !ok {
				continue
			}
			text = closedTruncationFieldName(text)
			if text == "" {
				continue
			}
			if _, exists := seen[text]; exists {
				continue
			}
			seen[text] = struct{}{}
			truncation.TruncatedFields = append(truncation.TruncatedFields, text)
		}
		sort.Strings(truncation.TruncatedFields)
	}
	if bytes, ok := readFailureInteger(value["serializedBytes"]); ok && bytes > 0 {
		truncation.SerializedBytes = int(bytes)
	}
	if minimal, ok := value["minimalFallback"].(bool); ok {
		truncation.MinimalFallback = minimal
	}
	if !truncation.DiagnosticsTruncated && truncation.OmittedCount == 0 && truncation.DroppedCount == 0 && len(truncation.TruncatedFields) == 0 && truncation.SerializedBytes == 0 && !truncation.MinimalFallback {
		return nil
	}
	return truncation
}

// closedTruncationFieldName returns the field name only when it belongs to the
// closed receipt vocabulary. Any other name fails closed and is
// dropped so a stale/malicious receipt cannot inject arbitrary field names. The
// name is matched byte-for-byte: a spaced variant of a valid field name is
// never trimmed into the accepted identity.
func closedTruncationFieldName(name string) string {
	switch name {
	case "code", "stage", "path", "problem", "hint", "message", "field", "key", "dependencyPath", "index":
		return name
	default:
		return ""
	}
}

// safeDurableJSON projects durable Run state JSON using typed, schema-known
// kernel failure locations. Only the public failure objects (State.failure,
// activations[].failure, scope parallel-branch failures, and fan-out WorkItem
// failures) are rewritten: a historical nested `message` is projected into a
// safe bounded code→problem/hint contract (unknown prose fails closed) and the
// raw message is dropped, while structured stage/path/retryable/diagnostics/
// workItem are retained. Operational keys are stripped only at the schema-known
// typed container levels. Arbitrary user-authored input/variables/output values
// are preserved EXACTLY, even when nested keys collide with failure, failureCode,
// failureMessage, or operational-key names such as ownerUserId/clientMessageId.
func safeDurableJSON(payload json.RawMessage) json.RawMessage {
	if len(bytes.TrimSpace(payload)) == 0 {
		return nil
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return nil
	}
	projectStateFailures(value)
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil
	}
	return encoded
}

// projectStateFailures walks the schema-known kernel State failure locations
// and projects each historical Failure object. It never descends into user
// input/variables/output Values, so arbitrary user content survives exactly even
// when its keys collide with failure or operational-key names.
func projectStateFailures(value any) {
	state, ok := value.(map[string]any)
	if !ok {
		return
	}
	stripOperationalKeysAtLevel(state)
	projectFailureField(state, "failure")
	projectActivationFailures(state["activations"])
	projectScopeFailures(state["scopes"])
}

// projectActivationFailures processes each schema-known Activation container.
// The activation's input/output Values are user-authored and never descended.
func projectActivationFailures(raw any) {
	list, ok := raw.([]any)
	if !ok {
		return
	}
	for _, item := range list {
		activation, ok := item.(map[string]any)
		if !ok {
			continue
		}
		stripOperationalKeysAtLevel(activation)
		projectFailureField(activation, "failure")
	}
}

// projectScopeFailures processes each schema-known Scope container: the
// parallel-branch failures and the fan-out WorkItem failures. Loop State and
// branch/work-item input/output Values are user-authored and never descended.
func projectScopeFailures(raw any) {
	list, ok := raw.([]any)
	if !ok {
		return
	}
	for _, item := range list {
		scope, ok := item.(map[string]any)
		if !ok {
			continue
		}
		stripOperationalKeysAtLevel(scope)
		if parallel, ok := scope["parallel"].(map[string]any); ok {
			stripOperationalKeysAtLevel(parallel)
			if branches, ok := parallel["branches"].([]any); ok {
				for _, branch := range branches {
					if branchState, ok := branch.(map[string]any); ok {
						stripOperationalKeysAtLevel(branchState)
						projectFailureField(branchState, "failure")
					}
				}
			}
		}
		if fanOut, ok := scope["fanOut"].(map[string]any); ok {
			stripOperationalKeysAtLevel(fanOut)
			if items, ok := fanOut["items"].([]any); ok {
				for _, item := range items {
					if workItem, ok := item.(map[string]any); ok {
						stripOperationalKeysAtLevel(workItem)
						projectFailureField(workItem, "failure")
					}
				}
			}
		}
	}
}

// projectFailureField projects a historical Failure object at a schema-known
// location through the unified typed allowlisted projector.
func projectFailureField(object map[string]any, key string) {
	failure, ok := object[key].(map[string]any)
	if !ok {
		return
	}
	object[key] = projectFailureContract(failure, readFailureTruncation(failure))
}

// Read-time bounds for the typed public-safe failure projection. They mirror
// the persisted public bounds so a malformed or oversized legacy row cannot
// push unbounded or operational text into the Run show/log/effect surfaces.
const (
	readFailureCodeRunes       = 128
	readFailurePathRunes       = 1024
	readFailureProblemRunes    = 1024
	readFailureHintRunes       = 1024
	readFailureMessageRunes    = 1024
	readFailureSerializedBytes = 32 * 1024
)

// projectFailureContract is THE single typed schema-known allowlisted projector
// for a durable Workflow failure object. It is used for Run/activation
// ErrorJSON, event payload failure objects, State failure objects, and
// EffectResult failure fields. Every public field is decoded with an explicit
// type check against a reviewed closed contract:
//
//   - problem/hint are always derived from the reviewed runtime code map — a
//     persisted explicit problem/hint is never trusted;
//   - code/stage/path are validated against the reviewed closed contracts;
//   - integers accept json.Number and concrete int forms only after
//     integrality/range checks;
//   - secret-bearing or malformed well-typed fields are rejected (fail closed);
//   - the shared diagnostic budget (runkernel.BoundFailureDiagnosticsCarried)
//     bounds the diagnostics array and deterministically merges prior+fresh
//     truncation for count/field/serialized/minimal-fallback cuts;
//   - the serialized projection is re-bounded; an oversized object fails closed
//     to the minimal safe {code, problem} contract.
//
// prior carries an earlier truncation report (from a persisted receipt) so a
// replay/re-read preserves the earlier cuts instead of recomputing them away.
func projectFailureContract(raw map[string]any, prior *runkernel.FailureTruncation) map[string]any {
	// code: validate the FULL raw value against the shared exact producer
	// grammar. An oversized or malformed code is NEVER truncated into a
	// different valid identity; it fails closed entirely.
	code, ok := runkernel.PublicFailureCode(readFailureString(raw["code"]))
	if !ok {
		return map[string]any{"code": "", "problem": "Workflow Run failed"}
	}
	projected := map[string]any{"code": code}
	// stage: shared closed producer vocabulary; unknown/secret-bearing fails.
	if stage, ok := runkernel.PublicFailureStage(readFailureString(raw["stage"])); ok {
		projected["stage"] = stage
	}
	// path: shared structured kernel path grammar; free-form prose fails closed.
	if path, ok := runkernel.PublicFailurePath(readFailureString(raw["path"])); ok {
		projected["path"] = path
	}
	// problem/hint are ALWAYS derived from the reviewed runtime code map.
	projected["problem"] = boundReadFailureString(runkernel.FailureProblem(code), readFailureProblemRunes)
	projected["hint"] = boundReadFailureString(runkernel.FailureHint(code), readFailureHintRunes)
	// retryable: strict bool type check; a non-bool value fails closed.
	if retryable, ok := raw["retryable"].(bool); ok {
		projected["retryable"] = retryable
	}
	// diagnostics: decode through the typed kernel contract and apply the shared
	// budget (count/field/serialized) with deterministic prior+fresh truncation
	// merge. Malformed elements fail closed (dropped) and are counted in the
	// report. An empty diagnostics array that carries a truncation report is the
	// honest projection of an all-dropped list and is preserved.
	if diagnostics, dropped, ok := readFailureDiagnostics(raw["diagnostics"]); ok {
		bounded, truncation := runkernel.BoundFailureDiagnosticsCarried(diagnostics, prior)
		if dropped > 0 {
			if truncation == nil {
				truncation = &runkernel.FailureTruncation{}
			}
			truncation.DroppedCount += dropped
			truncation.DiagnosticsTruncated = true
		}
		if truncation != nil {
			projected["truncation"] = marshalReadTruncation(truncation)
		}
		if len(bounded) > 0 {
			projected["diagnostics"] = marshalReadDiagnostics(bounded)
		} else if truncation != nil {
			projected["diagnostics"] = []any{}
		}
	} else if prior != nil {
		// A malformed diagnostics array still carries a prior truncation forward.
		projected["truncation"] = marshalReadTruncation(prior)
	}
	if workItem, ok := raw["workItem"].(map[string]any); ok {
		if bounded := projectReadWorkItem(workItem); bounded != nil {
			projected["workItem"] = bounded
		}
	}
	// Re-bound the serialized size: an oversized failure object fails closed to
	// the minimal safe contract and ALWAYS reports the minimal-fallback cut, even
	// for a fresh (no prior) oversized receipt.
	if encoded, err := json.Marshal(projected); err == nil && len(encoded) > readFailureSerializedBytes {
		report := &runkernel.FailureTruncation{
			DiagnosticsTruncated: true,
			MinimalFallback:      true,
		}
		if prior != nil {
			report.OmittedCount = prior.OmittedCount
			report.DroppedCount = prior.DroppedCount
			report.TruncatedFields = append([]string(nil), prior.TruncatedFields...)
		}
		fallback := map[string]any{"code": code, "problem": "Workflow Run failed"}
		fallback["truncation"] = marshalReadTruncation(report)
		return fallback
	}
	return projected
}

// readFailureString reads a string field with an explicit type check; a
// non-string value fails closed to an empty string.
func readFailureString(value any) string {
	text, ok := value.(string)
	if !ok {
		return ""
	}
	return text
}

// readFailureInteger reads an integer field accepting json.Number and concrete
// int forms only after integrality/range checks. Non-integral, out-of-range, or
// non-numeric values fail closed.
func readFailureInteger(value any) (int64, bool) {
	switch typed := value.(type) {
	case json.Number:
		integer, err := typed.Int64()
		return integer, err == nil
	case int:
		return int64(typed), true
	case int64:
		return typed, true
	case float64:
		if typed != math.Trunc(typed) || typed < math.MinInt64 || typed > math.MaxInt64 {
			return 0, false
		}
		return int64(typed), true
	default:
		return 0, false
	}
}

// readFailureDiagnostics decodes a raw diagnostics array into the typed kernel
// contract. Every element must be an object with a reviewed code; malformed
// elements fail closed (dropped). The returned dropped count reports how many
// malformed/secret-bearing elements were dropped so the single deterministic
// truncation report counts them. Returns false when the raw value is not an
// array at all.
func readFailureDiagnostics(raw any) ([]runkernel.FailureDiagnostic, int, bool) {
	list, ok := raw.([]any)
	if !ok {
		return nil, 0, false
	}
	diagnostics := make([]runkernel.FailureDiagnostic, 0, len(list))
	dropped := 0
	for _, item := range list {
		element, ok := item.(map[string]any)
		if !ok {
			dropped++
			continue
		}
		// A diagnostic code must be a FULL valid identity (including
		// valid hyphenated authored codes). Prose, punctuation, Unicode, spaced,
		// or secret-bearing values fail closed and are counted as dropped; an
		// oversized code is never truncated into a different valid identity.
		code, ok := runkernel.PublicFailureCode(readFailureString(element["code"]))
		if !ok {
			dropped++
			continue
		}
		diagnostic := runkernel.FailureDiagnostic{Code: code}
		if path, ok := runkernel.PublicFailurePath(readFailureString(element["path"])); ok {
			diagnostic.Path = path
		}
		if field, ok := runkernel.PublicDiagnosticField(readFailureString(element["field"])); ok {
			diagnostic.Field = field
		}
		if index, ok := readFailureInteger(element["index"]); ok && index >= 0 {
			value := int(index)
			diagnostic.Index = &value
		}
		if message := boundReadFailureString(readFailureString(element["message"]), readFailureMessageRunes); message != "" {
			diagnostic.Message = message
		}
		diagnostics = append(diagnostics, diagnostic)
	}
	return diagnostics, dropped, true
}

// marshalReadDiagnostics converts the typed bounded diagnostics back to the
// public-safe map shape.
func marshalReadDiagnostics(diagnostics []runkernel.FailureDiagnostic) []any {
	projected := make([]any, 0, len(diagnostics))
	for _, diagnostic := range diagnostics {
		bounded := map[string]any{"code": diagnostic.Code}
		if diagnostic.Path != "" {
			bounded["path"] = diagnostic.Path
		}
		if diagnostic.Field != "" {
			bounded["field"] = diagnostic.Field
		}
		if diagnostic.Index != nil {
			bounded["index"] = *diagnostic.Index
		}
		if diagnostic.Message != "" {
			bounded["message"] = diagnostic.Message
		}
		projected = append(projected, bounded)
	}
	return projected
}

// marshalReadTruncation converts the typed truncation report (already merged
// deterministically by the shared budget) to the public-safe map shape.
func marshalReadTruncation(truncation *runkernel.FailureTruncation) map[string]any {
	projected := map[string]any{}
	if truncation.DiagnosticsTruncated {
		projected["diagnosticsTruncated"] = true
	}
	if truncation.OmittedCount > 0 {
		projected["omittedCount"] = truncation.OmittedCount
	}
	if truncation.DroppedCount > 0 {
		projected["droppedCount"] = truncation.DroppedCount
	}
	if len(truncation.TruncatedFields) > 0 {
		fields := make([]any, 0, len(truncation.TruncatedFields))
		for _, field := range truncation.TruncatedFields {
			if field = closedTruncationFieldName(field); field == "" {
				continue
			}
			fields = append(fields, field)
		}
		if len(fields) > 0 {
			projected["truncatedFields"] = fields
		}
	}
	if truncation.SerializedBytes > 0 {
		projected["serializedBytes"] = truncation.SerializedBytes
	}
	if truncation.MinimalFallback {
		projected["minimalFallback"] = true
	}
	if len(projected) == 0 {
		return nil
	}
	return projected
}

// projectReadWorkItem type-validates and bounds a WorkItem object. Every
// public field is type-checked and rune-bounded; the index accepts json.Number
// and concrete integer forms after integrality/range checks; unknown keys are
// stripped.
func projectReadWorkItem(raw map[string]any) map[string]any {
	projected := map[string]any{}
	// WorkItem identity fields use the shared exact grammars; oversized or
	// malformed values fail closed instead of truncating into a different
	// identity.
	if key, ok := runkernel.PublicWorkItemKey(readFailureString(raw["key"])); ok {
		projected["key"] = key
	}
	if dependencyPath, ok := runkernel.ValidDependencyPath(readFailureString(raw["dependencyPath"])); ok {
		projected["dependencyPath"] = dependencyPath
	}
	if index, ok := readFailureInteger(raw["index"]); ok && index >= 0 {
		projected["index"] = int(index)
	}
	if len(projected) == 0 {
		return nil
	}
	return projected
}

// boundReadFailureString forces valid UTF-8, trims, and caps a failure field at
// the given rune bound.
func boundReadFailureString(value string, maxRunes int) string {
	value = strings.TrimSpace(strings.ToValidUTF8(value, "�"))
	if runes := []rune(value); len(runes) > maxRunes {
		return string(runes[:maxRunes])
	}
	return value
}

func scrubOperationalValue(value any) {
	switch typed := value.(type) {
	case map[string]any:
		for key, nested := range typed {
			if sensitiveOperationalKey(key) {
				delete(typed, key)
				continue
			}
			scrubOperationalValue(nested)
		}
	case []any:
		for _, nested := range typed {
			scrubOperationalValue(nested)
		}
	}
}

// stripOperationalKeysAtLevel removes infrastructure-only keys from ONE typed
// container map without recursing into nested values. It is used by the
// schema-aware State/EffectResult projections so user-authored input/variables/
// output (which may legitimately contain keys named ownerUserId, clientMessageId,
// leaseToken, etc.) are preserved exactly.
func stripOperationalKeysAtLevel(object map[string]any) {
	for key := range object {
		if sensitiveOperationalKey(key) {
			delete(object, key)
		}
	}
}

func sensitiveOperationalKey(value string) bool {
	normalized := strings.ToLower(strings.NewReplacer("_", "", "-", "").Replace(strings.TrimSpace(value)))
	switch normalized {
	case "owneruserid", "ownermemberid", "principalmemberid", "authormemberid",
		"executionowner", "executionidentity", "identitysnapshot",
		"leaseowner", "leasetoken", "leaseexpires", "leaseexpiresat",
		"idempotencykey", "clientmessageid":
		return true
	default:
		return false
	}
}
