package run

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

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

// safeEffectResultJSON projects a durable effect ResultJSON through the typed
// EffectResult boundary. A historical top-level `failureMessage` is projected
// into the safe bounded code→problem/hint contract (unknown prose fails closed)
// and dropped, while the failure fields and the `output` Values are preserved.
// Operational keys are stripped only at the EffectResult typed level, never
// inside the user-authored output.
func safeEffectResultJSON(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 typed EffectResult projector only accepts a flat object. A scalar,
	// array, or null top-level value is not a trustworthy EffectResult and fails
	// closed to nil (never marshaled back out unchanged).
	if _, ok := value.(map[string]any); !ok {
		return nil
	}
	projectEffectResultValue(value)
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil
	}
	return encoded
}

// projectEffectResultValue rewrites one flat EffectResult object: the raw
// `failureMessage`/`failureMessageRaw` are dropped and the complete flat
// EffectResult failure namespace (failureCode/failureStage/failurePath/
// failureDiagnostics/failureWorkItem/failureTruncation/failureRetryable) is
// routed through the unified typed projector so stage/path/diagnostics/workItem
// are validated against the reviewed closed contracts, secret-bearing or
// malformed values fail closed, and the shared diagnostic budget produces one
// deterministic prior+fresh truncation report. The reviewed failureCode derives
// the bounded failureProblem/failureHint (unknown prose fails closed). Unknown
// and operational keys are stripped in the non-user EffectResult data, while the
// user-authored `output` values are preserved exactly.
func projectEffectResultValue(value any) {
	result, ok := value.(map[string]any)
	if !ok {
		return
	}
	stripOperationalExceptUserKeys(result, map[string]struct{}{"output": {}})
	code, _ := result["failureCode"].(string)
	// The failureCode is an identity: validate the FULL raw value byte-for-byte
	// through the exact producer grammar. A spaced/malformed/oversized value
	// is never TrimSpace'd or truncated into a different valid identity; it fails
	// closed exactly like an empty code.
	publicCode, ok := runkernel.PublicFailureCode(code)
	if !ok || publicCode == "" {
		// No reviewed code: fail closed to the generic bounded problem and drop
		// the entire flat failure namespace (no stale stage/path/diagnostics may
		// survive without a reviewed code).
		delete(result, "failureCode")
		delete(result, "failureMessage")
		delete(result, "failureMessageRaw")
		delete(result, "failureStage")
		delete(result, "failurePath")
		delete(result, "failureProblem")
		delete(result, "failureHint")
		delete(result, "failureRetryable")
		delete(result, "failureDiagnostics")
		delete(result, "failureWorkItem")
		delete(result, "failureTruncation")
		// The flat EffectResult is still a closed typed schema for its non-failure
		// fields, even without a failureCode.
		projectEffectResultContext(result)
		return
	}
	code = publicCode
	// Build the public nested failure shape from the flat EffectResult
	// namespace and route it through the SAME unified typed projector used by
	// State/ErrorJSON/events so every closed grammar (stage/path/diagnostics/
	// workItem/truncation) and the shared budget apply identically.
	failure := map[string]any{
		"code":      code,
		"stage":     result["failureStage"],
		"path":      result["failurePath"],
		"problem":   result["failureProblem"],
		"hint":      result["failureHint"],
		"retryable": result["failureRetryable"],
	}
	if diagnostics, ok := result["failureDiagnostics"]; ok {
		failure["diagnostics"] = diagnostics
	}
	if workItem, ok := result["failureWorkItem"].(map[string]any); ok {
		failure["workItem"] = workItem
	}
	if truncation, ok := result["failureTruncation"].(map[string]any); ok {
		failure["truncation"] = truncation
	}
	projected := projectFailureContract(failure, readFailureTruncation(failure))
	// Write the projected public fields back into the flat EffectResult shape.
	delete(result, "failureCode")
	delete(result, "failureMessage")
	delete(result, "failureMessageRaw")
	delete(result, "failureStage")
	delete(result, "failurePath")
	delete(result, "failureProblem")
	delete(result, "failureHint")
	delete(result, "failureRetryable")
	delete(result, "failureDiagnostics")
	delete(result, "failureWorkItem")
	delete(result, "failureTruncation")
	result["failureCode"] = projected["code"]
	if stage, ok := projected["stage"].(string); ok && stage != "" {
		result["failureStage"] = stage
	}
	if path, ok := projected["path"].(string); ok && path != "" {
		result["failurePath"] = path
	}
	if problem, ok := projected["problem"].(string); ok && problem != "" {
		result["failureProblem"] = problem
	}
	if hint, ok := projected["hint"].(string); ok && hint != "" {
		result["failureHint"] = hint
	}
	if retryable, ok := projected["retryable"].(bool); ok {
		result["failureRetryable"] = retryable
	}
	if diagnostics, ok := projected["diagnostics"].([]any); ok && len(diagnostics) > 0 {
		result["failureDiagnostics"] = diagnostics
	}
	if workItem, ok := projected["workItem"].(map[string]any); ok && len(workItem) > 0 {
		result["failureWorkItem"] = workItem
	}
	if truncation, ok := projected["truncation"].(map[string]any); ok && len(truncation) > 0 {
		result["failureTruncation"] = truncation
	}
	// Close the flat EffectResult non-failure fields against the shared typed
	// schema: port/kind/status enums, valid message/notification/member/agent
	// IDs, message scopes, and RFC3339 retryAt. Malformed or unknown non-failure
	// fields fail closed; the user-authored output is preserved exactly.
	projectEffectResultContext(result)
}

// effectResultNonFailureContext is the allowlist of flat EffectResult non-failure
// fields with the same typed validators used by the event envelope schema. It
// matches orchestrator.EffectResult EXACTLY: the fields the orchestrator producer
// writes and nothing more. Invented non-producer fields (kind/status/retryAt/
// receiptRecovery) are NOT EffectResult fields and must not project.
var effectResultNonFailureContext = map[string]func(any) (any, bool){
	"port":                     envelopeFieldPlanPort,
	"messageId":                envelopeFieldMessageID(readEnvelopeIDRunes),
	"notificationId":           envelopeFieldStableChildID(validNotificationIDPrefix, readEnvelopeIDRunes),
	"notificationRevision":     envelopeFieldRevision,
	"completedAt":              envelopeFieldCompletedAt,
	"assigneeMemberId":         envelopeFieldRawMemberID(readEnvelopeIDRunes),
	"decidedByMemberId":        envelopeFieldRawMemberID(readEnvelopeIDRunes),
	"callbackResponderAgentId": envelopeFieldEntityID(validAgentIDPrefix, readEnvelopeIDRunes),
	"wakeAgentIds":             envelopeFieldAgentIDList(readEnvelopeIDRunes, readEnvelopeListCap),
	"deliveryScope":            envelopeFieldScope,
	"threadScope":              envelopeFieldScope,
	"workScope":                envelopeFieldScope,
}

// exactEffectResultFailureNamespace is the closed set of flat EffectResult
// failure-namespace keys that may survive. The projectEffectResultValue
// projector rewrites all of them into the typed bounded shape; ANY other
// `failure*` key (unknown legacy field, secret-bearing, corruption-controlled)
// is unknown and fails closed (dropped). This is an exact allowlist — never a
// `HasPrefix("failure")` exemption that lets arbitrary failure-prefixed secrets
// survive.
var exactEffectResultFailureNamespace = map[string]struct{}{
	"failureCode":        {},
	"failureMessage":     {},
	"failureMessageRaw":  {},
	"failureStage":       {},
	"failurePath":        {},
	"failureProblem":     {},
	"failureHint":        {},
	"failureRetryable":   {},
	"failureDiagnostics": {},
	"failureWorkItem":    {},
	"failureTruncation":  {},
}

// isKnownEffectResultFailureField reports whether key is a member of the exact
// closed flat EffectResult failure namespace.
func isKnownEffectResultFailureField(key string) bool {
	_, ok := exactEffectResultFailureNamespace[key]
	return ok
}

// projectEffectResultFailureFields projects the flat EffectResult failure
// namespace in an event envelope through the exact typed projector (the same
// projector the effect response uses), dropping unknown failure-prefixed and
// secret-bearing fields. It never inherits the effect-response open `output`
// exemption.
func projectEffectResultFailureFields(object map[string]any) {
	// Route the flat failure namespace through the shared projectEffectResultValue
	// so stage/path/diagnostics/workItem/truncation are validated and the raw
	// failureMessage is dropped, then remove any unknown `failure*` field the
	// projector did not emit (exact allowlist, never a prefix exemption).
	projectEffectResultValue(object)
	for key := range object {
		if key == "output" {
			// The event-envelope path never inherits the effect-response open
			// `output` exemption.
			delete(object, key)
			continue
		}
		if isKnownEffectResultFailureField(key) {
			continue
		}
		if strings.HasPrefix(key, "failure") {
			delete(object, key)
			continue
		}
		// Unknown non-failure context key in a flat failure event: drop it.
		delete(object, key)
	}
}

// projectEffectResultContext rewrites the flat EffectResult non-failure fields
// against the shared typed schema, dropping malformed, unknown, or secret-bearing
// values while preserving the user-authored output and the public failure
// namespace exactly.
func projectEffectResultContext(result map[string]any) {
	for key, extract := range effectResultNonFailureContext {
		if raw, present := result[key]; present {
			if value, ok := extract(raw); ok {
				result[key] = value
			} else {
				delete(result, key)
			}
		}
	}
	// Close the schema: every top-level field that is not part of the reviewed
	// typed allowlist, the EXACT closed failure namespace, or the user-authored
	// output is dropped. A `failure*` key outside the exact namespace is unknown
	// (legacy, secret-bearing, or corruption-controlled) and must NOT survive.
	for key := range result {
		if key == "output" {
			continue
		}
		if _, known := effectResultNonFailureContext[key]; known {
			continue
		}
		if isKnownEffectResultFailureField(key) {
			continue
		}
		if strings.HasPrefix(key, "failure") {
			// Unknown failure-prefixed field: fail closed, never project.
			delete(result, key)
			continue
		}
		delete(result, key)
	}
}

// stripOperationalExceptUserKeys strips infrastructure-only keys recursively
// from a container while preserving the named user Values exactly. The
// user-authored values under userKeys (and everything nested inside them) are
// never touched, even when their keys collide with operational-key names.
func stripOperationalExceptUserKeys(object map[string]any, userKeys map[string]struct{}) {
	for key, nested := range object {
		if sensitiveOperationalKey(key) {
			delete(object, key)
			continue
		}
		if _, user := userKeys[key]; user {
			continue
		}
		scrubOperationalValue(nested)
	}
}

// safeErrorCode projects one persisted failure-code identity through the
// exact full-value producer grammar. A legitimate stable code survives
// exactly; a malformed, punctuation/token-bearing, invalid-UTF8, or oversized
// value fails closed (dropped, never truncated into a different identity). It is
// the shared top-level ErrorCode projection used by every human and trusted-Agent
// Run/currentStep/Activation/Effect DTO path, disjoint from the event-payload
// schemas.
func safeErrorCode(value string) string {
	code, ok := runkernel.PublicFailureCode(value)
	if !ok {
		return ""
	}
	return code
}
