package streamgate

import (
	"errors"
	"strconv"
	"time"
)

// FilterDecisionKind identifies the semantic outcome of a filter evaluation.
type FilterDecisionKind string

const (
	// FilterDecisionKindPass indicates the event passed the filter and may be
	// released downstream.
	FilterDecisionKindPass FilterDecisionKind = "pass"

	// FilterDecisionKindObserve indicates the filter observed the event but
	// does not block release.
	FilterDecisionKindObserve FilterDecisionKind = "observe"

	// FilterDecisionKindViolation indicates the event violated a filter rule.
	FilterDecisionKindViolation FilterDecisionKind = "violation"

	// FilterDecisionKindFatal indicates a fatal filter failure that must
	// terminate the stream.
	FilterDecisionKindFatal FilterDecisionKind = "fatal"

	// FilterDecisionKindReplacement indicates the event should be replaced
	// with a safe alternative.
	FilterDecisionKindReplacement FilterDecisionKind = "replacement"
)

// knownFilterDecisionKinds is the closed set of valid FilterDecisionKind values.
var knownFilterDecisionKinds = map[FilterDecisionKind]struct{}{
	FilterDecisionKindPass:        {},
	FilterDecisionKindObserve:     {},
	FilterDecisionKindViolation:   {},
	FilterDecisionKindFatal:       {},
	FilterDecisionKindReplacement: {},
}

// FilterDecision records whether the stream gate filter evaluated this event
// for the current epoch. It carries stable identity, bounded sanitized
// evidence, and an optional recovery intent or replacement proposal. Only the
// violation decision may carry a recovery intent, and only a replacement
// decision may carry a replacement proposal.
type FilterDecision struct {
	kind        FilterDecisionKind
	consumerID  StableToken
	filterID    StableToken
	ruleID      StableToken
	evidence    SanitizedEvidence
	intent      *RecoveryIntent
	replacement *ReplacementProposal
}

// NewFilterDecision creates a FilterDecision with validation. The returned
// value is immutable to callers. Only a violation decision may carry a
// recovery intent; all other kinds reject a non-nil intent.
func NewFilterDecision(
	kind FilterDecisionKind,
	consumerID, filterID, ruleID string,
	evidence SanitizedEvidence,
	intent *RecoveryIntent,
) (FilterDecision, error) {
	return NewFilterDecisionWithReplacement(kind, consumerID, filterID, ruleID, evidence, intent, nil)
}

// NewFilterDecisionWithReplacement creates a FilterDecision with an optional
// replacement proposal.
func NewFilterDecisionWithReplacement(
	kind FilterDecisionKind,
	consumerID, filterID, ruleID string,
	evidence SanitizedEvidence,
	intent *RecoveryIntent,
	replacement *ReplacementProposal,
) (FilterDecision, error) {
	if _, ok := knownFilterDecisionKinds[kind]; !ok {
		return FilterDecision{}, errors.New("streamgate: filter decision unknown kind")
	}
	c, err := NewStableTokenRequired("consumerID", consumerID)
	if err != nil {
		return FilterDecision{}, err
	}
	f, err := NewStableTokenRequired("filterID", filterID)
	if err != nil {
		return FilterDecision{}, err
	}
	r, err := NewStableTokenRequired("ruleID", ruleID)
	if err != nil {
		return FilterDecision{}, err
	}
	if err := evidence.Validate(); err != nil {
		return FilterDecision{}, err
	}
	var intentCopy *RecoveryIntent
	if intent != nil {
		cp := *intent
		intentCopy = &cp
	}
	var replacementCopy *ReplacementProposal
	if replacement != nil {
		cp := cloneReplacementProposal(replacement)
		replacementCopy = cp
	}
	decision := FilterDecision{
		kind:        kind,
		consumerID:  c,
		filterID:    f,
		ruleID:      r,
		evidence:    evidence,
		intent:      intentCopy,
		replacement: replacementCopy,
	}
	if err := decision.Validate(); err != nil {
		return FilterDecision{}, err
	}
	return decision, nil
}

// Validate returns nil when the FilterDecision is in a consistent state.
func (d FilterDecision) Validate() error {
	if d.kind == "" {
		return errors.New("streamgate: filter decision kind is required")
	}
	if _, ok := knownFilterDecisionKinds[d.kind]; !ok {
		return errors.New("streamgate: filter decision unknown kind")
	}
	if err := validateStableTokenRequired("consumerID", d.consumerID.value); err != nil {
		return err
	}
	if err := validateStableTokenRequired("filterID", d.filterID.value); err != nil {
		return err
	}
	if err := validateStableTokenRequired("ruleID", d.ruleID.value); err != nil {
		return err
	}
	if err := d.evidence.Validate(); err != nil {
		return err
	}
	if d.intent != nil {
		if d.kind != FilterDecisionKindViolation {
			return errors.New("streamgate: only violation decision may carry a recovery intent")
		}
		if err := d.intent.Validate(); err != nil {
			return err
		}
	}
	if d.replacement != nil {
		if d.kind != FilterDecisionKindReplacement {
			return errors.New("streamgate: only replacement decision may carry a replacement proposal")
		}
		if err := d.replacement.Validate(); err != nil {
			return err
		}
	}
	return nil
}

// Kind returns the filter decision kind.
func (d FilterDecision) Kind() FilterDecisionKind { return d.kind }

// ConsumerID returns the filter consumer id token.
func (d FilterDecision) ConsumerID() string { return d.consumerID.value }

// FilterID returns the stable filter id token.
func (d FilterDecision) FilterID() string { return d.filterID.value }

// RuleID returns the rule id token.
func (d FilterDecision) RuleID() string { return d.ruleID.value }

// Evidence returns a defensive copy of the sanitized evidence.
func (d FilterDecision) Evidence() SanitizedEvidence {
	return d.evidence
}

// RecoveryIntent returns the recovery intent, or nil if not set.
func (d FilterDecision) RecoveryIntent() *RecoveryIntent {
	return d.Intent()
}

// Intent returns the recovery intent, or nil if not set.
func (d FilterDecision) Intent() *RecoveryIntent {
	if d.intent == nil {
		return nil
	}
	cp := *d.intent
	return &cp
}

// ReplacementProposal returns the replacement proposal, or nil if not set.
func (d FilterDecision) ReplacementProposal() *ReplacementProposal {
	return cloneReplacementProposal(d.replacement)
}

// ReplacementProposal carries an ordered sequence of replacement events to be released in place of the original event.
type ReplacementProposal struct {
	events []NormalizedEvent
}

// NewReplacementProposal constructs a validated ReplacementProposal.
func NewReplacementProposal(events []NormalizedEvent) (ReplacementProposal, error) {
	if len(events) == 0 {
		return ReplacementProposal{}, errors.New("streamgate: replacement proposal events must not be empty")
	}
	cp := make([]NormalizedEvent, len(events))
	for i, ev := range events {
		if err := ev.Validate(); err != nil {
			return ReplacementProposal{}, errors.New("streamgate: replacement proposal event " + strconv.Itoa(i) + ": " + err.Error())
		}
		cp[i] = cloneNormalizedEvent(ev)
	}
	return ReplacementProposal{events: cp}, nil
}

// Validate returns nil when the ReplacementProposal is in a consistent state.
func (r ReplacementProposal) Validate() error {
	if len(r.events) == 0 {
		return errors.New("streamgate: replacement proposal events must not be empty")
	}
	for i, ev := range r.events {
		if err := ev.Validate(); err != nil {
			return errors.New("streamgate: replacement proposal event " + strconv.Itoa(i) + ": " + err.Error())
		}
	}
	return nil
}

// Events returns a defensive deep copy of the replacement events.
func (r ReplacementProposal) Events() []NormalizedEvent {
	if r.events == nil {
		return nil
	}
	cp := make([]NormalizedEvent, len(r.events))
	for i, ev := range r.events {
		cp[i] = cloneNormalizedEvent(ev)
	}
	return cp
}

func cloneReplacementProposal(p *ReplacementProposal) *ReplacementProposal {
	if p == nil {
		return nil
	}
	cp, _ := NewReplacementProposal(p.Events())
	return &cp
}

// FilterOutcomeKind identifies the state of a filter outcome.
type FilterOutcomeKind string

const (
	// FilterOutcomeKindEvaluated indicates the filter was evaluated and
	// produced a concrete decision.
	FilterOutcomeKindEvaluated FilterOutcomeKind = "evaluated"

	// FilterOutcomeKindEvaluationError indicates the filter evaluation
	// failed with a sanitized error code.
	FilterOutcomeKindEvaluationError FilterOutcomeKind = "evaluation_error"

	// FilterOutcomeKindNotApplicableForEpoch indicates the filter did not
	// apply because the current epoch does not require evaluation.
	FilterOutcomeKindNotApplicableForEpoch FilterOutcomeKind = "not_applicable_for_epoch"

	// FilterOutcomeKindDeferredByRequirement indicates the filter deferred
	// evaluation because a required condition was not yet met.
	FilterOutcomeKindDeferredByRequirement FilterOutcomeKind = "deferred_by_requirement"
)

// Validate returns nil when the FilterOutcomeKind is a known lifecycle value.
func (k FilterOutcomeKind) Validate() error {
	switch k {
	case FilterOutcomeKindEvaluated, FilterOutcomeKindEvaluationError,
		FilterOutcomeKindNotApplicableForEpoch, FilterOutcomeKindDeferredByRequirement:
		return nil
	}
	return errors.New("streamgate: unknown filter outcome kind: " + string(k))
}

// knownFilterOutcomeKinds is the closed set of valid FilterOutcomeKind values.
var knownFilterOutcomeKinds = map[FilterOutcomeKind]struct{}{
	FilterOutcomeKindEvaluated:             {},
	FilterOutcomeKindEvaluationError:       {},
	FilterOutcomeKindNotApplicableForEpoch: {},
	FilterOutcomeKindDeferredByRequirement: {},
}

// FilterOutcome records the result of a filter evaluation. Exactly one of
// Decision or ErrorCode is set depending on the kind.
type FilterOutcome struct {
	kind      FilterOutcomeKind
	decision  *FilterDecision
	errorCode StableToken
}

// NewFilterOutcomeEvaluated creates a FilterOutcome of kind evaluated with
// the given decision.
func NewFilterOutcomeEvaluated(decision FilterDecision) (FilterOutcome, error) {
	if err := decision.Validate(); err != nil {
		return FilterOutcome{}, err
	}
	outcome := FilterOutcome{
		kind:     FilterOutcomeKindEvaluated,
		decision: &decision,
	}
	if err := outcome.Validate(); err != nil {
		return FilterOutcome{}, err
	}
	return outcome, nil
}

// NewFilterOutcomeEvaluationError creates a FilterOutcome of kind
// evaluation_error with a sanitized stable error code.
func NewFilterOutcomeEvaluationError(errorCode string) (FilterOutcome, error) {
	if errorCode == "" {
		return FilterOutcome{}, errors.New("streamgate: filter outcome error code is required")
	}
	c, err := NewStableTokenRequired("errorCode", errorCode)
	if err != nil {
		return FilterOutcome{}, err
	}
	outcome := FilterOutcome{
		kind:      FilterOutcomeKindEvaluationError,
		errorCode: c,
	}
	if err := outcome.Validate(); err != nil {
		return FilterOutcome{}, err
	}
	return outcome, nil
}

// NewFilterOutcomeNotApplicableForEpoch creates a FilterOutcome of kind
// not_applicable_for_epoch.
func NewFilterOutcomeNotApplicableForEpoch() FilterOutcome {
	return FilterOutcome{
		kind: FilterOutcomeKindNotApplicableForEpoch,
	}
}

// NewFilterOutcomeDeferredByRequirement creates a FilterOutcome of kind
// deferred_by_requirement.
func NewFilterOutcomeDeferredByRequirement() FilterOutcome {
	return FilterOutcome{
		kind: FilterOutcomeKindDeferredByRequirement,
	}
}

// Validate returns nil when the FilterOutcome is in a consistent state.
func (o FilterOutcome) Validate() error {
	if o.kind == "" {
		return errors.New("streamgate: filter outcome kind is required")
	}
	if _, ok := knownFilterOutcomeKinds[o.kind]; !ok {
		return errors.New("streamgate: filter outcome unknown kind")
	}
	switch o.kind {
	case FilterOutcomeKindEvaluated:
		if o.decision == nil {
			return errors.New("streamgate: evaluated filter outcome requires a decision")
		}
		if o.errorCode.value != "" {
			return errors.New("streamgate: evaluated filter outcome must not have an error code")
		}
		if err := o.decision.Validate(); err != nil {
			return err
		}
	case FilterOutcomeKindEvaluationError:
		if o.decision != nil {
			return errors.New("streamgate: evaluation error filter outcome must not have a decision")
		}
		if o.errorCode.value == "" {
			return errors.New("streamgate: evaluation error filter outcome requires an error code")
		}
		if err := validateStableTokenRequired("errorCode", o.errorCode.value); err != nil {
			return err
		}
	case FilterOutcomeKindNotApplicableForEpoch:
		if o.decision != nil {
			return errors.New("streamgate: not-applicable filter outcome must not have a decision")
		}
		if o.errorCode.value != "" {
			return errors.New("streamgate: not-applicable filter outcome must not have an error code")
		}
	case FilterOutcomeKindDeferredByRequirement:
		if o.decision != nil {
			return errors.New("streamgate: deferred filter outcome must not have a decision")
		}
		if o.errorCode.value != "" {
			return errors.New("streamgate: deferred filter outcome must not have an error code")
		}
	default:
		return errors.New("streamgate: filter outcome unknown kind")
	}
	return nil
}

// Kind returns the filter outcome kind.
func (o FilterOutcome) Kind() FilterOutcomeKind { return o.kind }

// Decision returns the filter decision, or nil if the outcome is not
// evaluated.
func (o FilterOutcome) Decision() *FilterDecision {
	if o.decision == nil {
		return nil
	}
	cp := *o.decision
	return &cp
}

// ErrorCode returns the sanitized error code token.
func (o FilterOutcome) ErrorCode() string { return o.errorCode.value }

// RecoveryStrategy identifies the recovery approach.
type RecoveryStrategy string

const (
	// RecoveryStrategyExactReplay indicates an exact replay of the original
	// request.
	RecoveryStrategyExactReplay RecoveryStrategy = "exact_replay"

	// RecoveryStrategyContinuationRepair indicates a continuation repair that
	// preserves the already-released safe prefix.
	RecoveryStrategyContinuationRepair RecoveryStrategy = "continuation_repair"

	// RecoveryStrategySchemaRepair indicates a schema repair that applies a
	// typed patch to the request.
	RecoveryStrategySchemaRepair RecoveryStrategy = "schema_repair"

	// RecoveryStrategyManagedContinuation indicates provider output-cap
	// progress that preserves the already-released safe prefix. It uses the
	// continuation directive but a trajectory budget separate from fault recovery.
	RecoveryStrategyManagedContinuation RecoveryStrategy = "managed_continuation"
)

// knownRecoveryStrategies is the closed set of valid RecoveryStrategy values.
var knownRecoveryStrategies = map[RecoveryStrategy]struct{}{
	RecoveryStrategyExactReplay:         {},
	RecoveryStrategyContinuationRepair:  {},
	RecoveryStrategySchemaRepair:        {},
	RecoveryStrategyManagedContinuation: {},
}

// RecoveryDirectiveKind identifies the directive type that must match the
// recovery strategy.
type RecoveryDirectiveKind string

const (
	// RecoveryDirectiveKindExact matches RecoveryStrategyExactReplay.
	RecoveryDirectiveKindExact RecoveryDirectiveKind = "exact"

	// RecoveryDirectiveKindContinuation matches RecoveryStrategyContinuationRepair
	// and RecoveryStrategyManagedContinuation.
	RecoveryDirectiveKindContinuation RecoveryDirectiveKind = "continuation"

	// RecoveryDirectiveKindSchema matches RecoveryStrategySchemaRepair.
	RecoveryDirectiveKindSchema RecoveryDirectiveKind = "schema"
)

// knownRecoveryDirectiveKinds is the closed set of valid RecoveryDirectiveKind values.
var knownRecoveryDirectiveKinds = map[RecoveryDirectiveKind]struct{}{
	RecoveryDirectiveKindExact:        {},
	RecoveryDirectiveKindContinuation: {},
	RecoveryDirectiveKindSchema:       {},
}

// RecoveryDirective carries a typed repair directive. Exactly one kind must
// match the recovery strategy. It contains only safe, sanitized information.
// No raw body, prefix, or patch text is preserved.
type RecoveryDirective struct {
	kind        RecoveryDirectiveKind
	requestRef  StableToken
	cursor      int
	snapshotRef StableToken
	schemaRef   StableToken
	patchCode   StableToken
}

// NewRecoveryDirectiveExact creates a RecoveryDirective of kind exact.
func NewRecoveryDirectiveExact(requestID string) (RecoveryDirective, error) {
	ref, err := NewStableTokenRequired("requestRef", requestID)
	if err != nil {
		return RecoveryDirective{}, err
	}
	directive := RecoveryDirective{
		kind:       RecoveryDirectiveKindExact,
		requestRef: ref,
	}
	if err := directive.Validate(); err != nil {
		return RecoveryDirective{}, err
	}
	return directive, nil
}

// NewRecoveryDirectiveContinuation creates a RecoveryDirective of kind
// continuation. It preserves only a cursor and bounded snapshot reference.
func NewRecoveryDirectiveContinuation(cursor int, snapshotRef string) (RecoveryDirective, error) {
	if cursor < 0 {
		return RecoveryDirective{}, errors.New("streamgate: continuation recovery directive cursor must be non-negative")
	}
	snapRef, err := NewStableTokenRequired("snapshotRef", snapshotRef)
	if err != nil {
		return RecoveryDirective{}, err
	}
	directive := RecoveryDirective{
		kind:        RecoveryDirectiveKindContinuation,
		cursor:      cursor,
		snapshotRef: snapRef,
	}
	if err := directive.Validate(); err != nil {
		return RecoveryDirective{}, err
	}
	return directive, nil
}

// NewRecoveryDirectiveSchema creates a RecoveryDirective of kind schema. It
// preserves only a stable schema reference and patch code.
func NewRecoveryDirectiveSchema(schemaRef, patchCode string) (RecoveryDirective, error) {
	schema, err := NewStableTokenRequired("schemaRef", schemaRef)
	if err != nil {
		return RecoveryDirective{}, err
	}
	patch, err := NewStableTokenRequired("patchCode", patchCode)
	if err != nil {
		return RecoveryDirective{}, err
	}
	directive := RecoveryDirective{
		kind:      RecoveryDirectiveKindSchema,
		schemaRef: schema,
		patchCode: patch,
	}
	if err := directive.Validate(); err != nil {
		return RecoveryDirective{}, err
	}
	return directive, nil
}

// Validate returns nil when the RecoveryDirective is in a consistent state.
func (d RecoveryDirective) Validate() error {
	if d.kind == "" {
		return errors.New("streamgate: recovery directive kind is required")
	}
	if _, ok := knownRecoveryDirectiveKinds[d.kind]; !ok {
		return errors.New("streamgate: recovery directive unknown kind")
	}
	switch d.kind {
	case RecoveryDirectiveKindExact:
		if d.requestRef.value == "" {
			return errors.New("streamgate: exact recovery directive request ref is required")
		}
		if err := validateStableTokenRequired("requestRef", d.requestRef.value); err != nil {
			return err
		}
		if d.cursor != 0 {
			return errors.New("streamgate: exact recovery directive must not have a cursor")
		}
		if d.snapshotRef.value != "" {
			return errors.New("streamgate: exact recovery directive must not have a snapshot ref")
		}
		if d.schemaRef.value != "" {
			return errors.New("streamgate: exact recovery directive must not have a schema ref")
		}
		if d.patchCode.value != "" {
			return errors.New("streamgate: exact recovery directive must not have a patch code")
		}
	case RecoveryDirectiveKindContinuation:
		if d.cursor < 0 {
			return errors.New("streamgate: continuation recovery directive cursor must be non-negative")
		}
		if d.snapshotRef.value == "" {
			return errors.New("streamgate: continuation recovery directive snapshot ref is required")
		}
		if err := validateStableTokenRequired("snapshotRef", d.snapshotRef.value); err != nil {
			return err
		}
		if d.requestRef.value != "" {
			return errors.New("streamgate: continuation recovery directive must not have a request ref")
		}
		if d.schemaRef.value != "" {
			return errors.New("streamgate: continuation recovery directive must not have a schema ref")
		}
		if d.patchCode.value != "" {
			return errors.New("streamgate: continuation recovery directive must not have a patch code")
		}
	case RecoveryDirectiveKindSchema:
		if d.schemaRef.value == "" {
			return errors.New("streamgate: schema recovery directive schema ref is required")
		}
		if err := validateStableTokenRequired("schemaRef", d.schemaRef.value); err != nil {
			return err
		}
		if d.patchCode.value == "" {
			return errors.New("streamgate: schema recovery directive patch code is required")
		}
		if err := validateStableTokenRequired("patchCode", d.patchCode.value); err != nil {
			return err
		}
		if d.requestRef.value != "" {
			return errors.New("streamgate: schema recovery directive must not have a request ref")
		}
		if d.cursor != 0 {
			return errors.New("streamgate: schema recovery directive must not have a cursor")
		}
		if d.snapshotRef.value != "" {
			return errors.New("streamgate: schema recovery directive must not have a snapshot ref")
		}
	default:
		return errors.New("streamgate: recovery directive unknown kind")
	}
	return nil
}

// Kind returns the recovery directive kind.
func (d RecoveryDirective) Kind() RecoveryDirectiveKind { return d.kind }

// MatchStrategy returns true when the directive kind matches the recovery
// strategy.
func (d RecoveryDirective) MatchStrategy(strategy RecoveryStrategy) bool {
	switch strategy {
	case RecoveryStrategyExactReplay:
		return d.kind == RecoveryDirectiveKindExact
	case RecoveryStrategyContinuationRepair, RecoveryStrategyManagedContinuation:
		return d.kind == RecoveryDirectiveKindContinuation
	case RecoveryStrategySchemaRepair:
		return d.kind == RecoveryDirectiveKindSchema
	default:
		return false
	}
}

// RequestRef returns the exact recovery directive request reference, or ""
// if not applicable.
func (d RecoveryDirective) RequestRef() string { return d.requestRef.value }

// Cursor returns the continuation recovery directive cursor.
func (d RecoveryDirective) Cursor() int { return d.cursor }

// SnapshotRef returns the continuation recovery directive snapshot reference,
// or "" if not applicable.
func (d RecoveryDirective) SnapshotRef() string { return d.snapshotRef.value }

// SchemaRef returns the schema recovery directive schema reference, or "" if
// not applicable.
func (d RecoveryDirective) SchemaRef() string { return d.schemaRef.value }

// PatchCode returns the schema recovery directive patch code, or "" if not
// applicable.
func (d RecoveryDirective) PatchCode() string { return d.patchCode.value }

// RecoveryIntent captures the host's intent for how to recover from a filter
// failure. It carries a typed strategy, a matching directive, sanitized reason,
// and filter priority.
type RecoveryIntent struct {
	strategy  RecoveryStrategy
	directive RecoveryDirective
	reason    StableToken
	priority  int
}

// NewRecoveryIntent creates a RecoveryIntent with validation. The returned
// value is immutable to callers.
func NewRecoveryIntent(strategy RecoveryStrategy, directive RecoveryDirective, reason string, priority int) (RecoveryIntent, error) {
	if _, ok := knownRecoveryStrategies[strategy]; !ok {
		return RecoveryIntent{}, errors.New("streamgate: recovery intent unknown strategy")
	}
	if reason == "" {
		return RecoveryIntent{}, errors.New("streamgate: recovery intent reason is required")
	}
	r, err := NewStableTokenRequired("reason", reason)
	if err != nil {
		return RecoveryIntent{}, err
	}
	if priority < 0 {
		return RecoveryIntent{}, errors.New("streamgate: recovery intent priority must be non-negative")
	}
	if err := directive.Validate(); err != nil {
		return RecoveryIntent{}, err
	}
	if !directive.MatchStrategy(strategy) {
		return RecoveryIntent{}, errors.New("streamgate: recovery directive kind does not match strategy")
	}
	intent := RecoveryIntent{
		strategy:  strategy,
		directive: directive,
		reason:    r,
		priority:  priority,
	}
	if err := intent.Validate(); err != nil {
		return RecoveryIntent{}, err
	}
	return intent, nil
}

// Validate returns nil when the RecoveryIntent is in a consistent state.
func (r RecoveryIntent) Validate() error {
	if r.strategy == "" {
		return errors.New("streamgate: recovery intent strategy is required")
	}
	if _, ok := knownRecoveryStrategies[r.strategy]; !ok {
		return errors.New("streamgate: recovery intent unknown strategy")
	}
	if r.reason.value == "" {
		return errors.New("streamgate: recovery intent reason is required")
	}
	if err := validateStableTokenRequired("reason", r.reason.value); err != nil {
		return err
	}
	if r.priority < 0 {
		return errors.New("streamgate: recovery intent priority must be non-negative")
	}
	if err := r.directive.Validate(); err != nil {
		return err
	}
	if !r.directive.MatchStrategy(r.strategy) {
		return errors.New("streamgate: recovery directive kind does not match strategy")
	}
	return nil
}

// Strategy returns the recovery strategy.
func (r RecoveryIntent) Strategy() RecoveryStrategy { return r.strategy }

// Directive returns the recovery directive.
func (r RecoveryIntent) Directive() RecoveryDirective { return r.directive }

// Reason returns the sanitized reason token.
func (r RecoveryIntent) Reason() string { return r.reason.value }

// Priority returns the filter priority.
func (r RecoveryIntent) Priority() int { return r.priority }

// FixedFingerprint is a fixed-size [32]byte digest value used to identify
// filtered events without preserving any raw body content. A zero digest is
// invalid.
type FixedFingerprint [32]byte

// ZeroFixedFingerprint is the zero-valued fingerprint used as a sentinel.
var ZeroFixedFingerprint FixedFingerprint

// Validate returns nil when the fingerprint is non-zero.
func (fp FixedFingerprint) Validate() error {
	for _, b := range fp {
		if b != 0 {
			return nil
		}
	}
	return errors.New("streamgate: sanitized evidence fingerprint must be non-zero")
}

// Bytes returns the fingerprint as a byte slice.
func (fp FixedFingerprint) Bytes() [32]byte { return fp }

// SanitizedEvidence carries only safe, external-facing information about an
// event that was filtered. It must not contain raw stack traces, provider
// body content, prompts, outputs, or authentication material. Only stable
// descriptor codes, fixed/bounded fingerprints, counts, and offsets are
// permitted.
type SanitizedEvidence struct {
	eventKind      EventKind
	channel        string
	filterRule     StableToken
	outcome        FilterOutcomeKind
	descriptorCode StableToken
	fingerprint    FixedFingerprint
	count          int
	offset         int
	timestamp      time.Time
}

// NewSanitizedEvidence creates a SanitizedEvidence with validation. The
// returned value is immutable to callers.
func NewSanitizedEvidence(
	kind EventKind,
	channel, filterRule, descriptorCode string,
	fingerprint FixedFingerprint,
	count, offset int,
	outcome FilterOutcomeKind,
	ts time.Time,
) (SanitizedEvidence, error) {
	if kind == "" {
		return SanitizedEvidence{}, errors.New("streamgate: sanitized evidence event kind is required")
	}
	if channel == "" {
		return SanitizedEvidence{}, errors.New("streamgate: sanitized evidence channel is required")
	}
	fr, err := NewStableTokenRequired("filterRule", filterRule)
	if err != nil {
		return SanitizedEvidence{}, err
	}
	dc, err := NewStableTokenRequired("descriptorCode", descriptorCode)
	if err != nil {
		return SanitizedEvidence{}, err
	}
	if err := kind.Validate(); err != nil {
		return SanitizedEvidence{}, err
	}
	if err := outcome.Validate(); err != nil {
		return SanitizedEvidence{}, err
	}
	if err := fingerprint.Validate(); err != nil {
		return SanitizedEvidence{}, err
	}
	if count < 0 {
		return SanitizedEvidence{}, errors.New("streamgate: sanitized evidence count must be non-negative")
	}
	if offset < 0 {
		return SanitizedEvidence{}, errors.New("streamgate: sanitized evidence offset must be non-negative")
	}
	if ts.IsZero() {
		return SanitizedEvidence{}, errors.New("streamgate: sanitized evidence timestamp is required")
	}
	evidence := SanitizedEvidence{
		eventKind:      kind,
		channel:        channel,
		filterRule:     fr,
		outcome:        outcome,
		descriptorCode: dc,
		fingerprint:    fingerprint,
		count:          count,
		offset:         offset,
		timestamp:      ts,
	}
	if err := evidence.Validate(); err != nil {
		return SanitizedEvidence{}, err
	}
	return evidence, nil
}

// Validate returns nil when the SanitizedEvidence is in a consistent state.
func (se SanitizedEvidence) Validate() error {
	if se.eventKind == "" {
		return errors.New("streamgate: sanitized evidence event kind is required")
	}
	if err := se.eventKind.Validate(); err != nil {
		return err
	}
	if se.channel == "" {
		return errors.New("streamgate: sanitized evidence channel is required")
	}
	if se.filterRule.value == "" {
		return errors.New("streamgate: sanitized evidence filter rule is required")
	}
	if err := validateStableTokenRequired("filterRule", se.filterRule.value); err != nil {
		return err
	}
	if se.descriptorCode.value == "" {
		return errors.New("streamgate: sanitized evidence descriptor code is required")
	}
	if err := validateStableTokenRequired("descriptorCode", se.descriptorCode.value); err != nil {
		return err
	}
	if err := se.fingerprint.Validate(); err != nil {
		return err
	}
	if se.count < 0 {
		return errors.New("streamgate: sanitized evidence count must be non-negative")
	}
	if se.offset < 0 {
		return errors.New("streamgate: sanitized evidence offset must be non-negative")
	}
	if err := se.outcome.Validate(); err != nil {
		return err
	}
	if se.timestamp.IsZero() {
		return errors.New("streamgate: sanitized evidence timestamp is required")
	}
	return nil
}

// EventKind returns the event kind.
func (se SanitizedEvidence) EventKind() EventKind { return se.eventKind }

// Channel returns the channel.
func (se SanitizedEvidence) Channel() string { return se.channel }

// FilterRule returns the filter rule id token.
func (se SanitizedEvidence) FilterRule() string { return se.filterRule.value }

// Outcome returns the filter outcome kind.
func (se SanitizedEvidence) Outcome() FilterOutcomeKind { return se.outcome }

// DescriptorCode returns the stable descriptor code token.
func (se SanitizedEvidence) DescriptorCode() string { return se.descriptorCode.value }

// Fingerprint returns the fixed/bounded fingerprint.
func (se SanitizedEvidence) Fingerprint() FixedFingerprint { return se.fingerprint }

// Count returns the bounded count.
func (se SanitizedEvidence) Count() int { return se.count }

// Offset returns the bounded offset.
func (se SanitizedEvidence) Offset() int { return se.offset }

// Timestamp returns the timestamp.
func (se SanitizedEvidence) Timestamp() time.Time { return se.timestamp }

// cloneResponseStart returns a deep copy of the ResponseStart including a
// defensive copy of its headers map.
func cloneResponseStart(rs *ResponseStart) *ResponseStart {
	if rs == nil {
		return nil
	}
	cp := *rs
	if rs.headers != nil {
		hcp := make(map[string]string, len(rs.headers))
		for k, v := range rs.headers {
			hcp[k] = v
		}
		cp.headers = hcp
	}
	return &cp
}

// cloneNormalizedEvent returns a deep copy of a NormalizedEvent, including
// defensive copies of nested maps, descriptors, and cause chains.
func cloneNormalizedEvent(e NormalizedEvent) NormalizedEvent {
	cp := e
	if e.headers != nil {
		hcp := make(map[string]string, len(e.headers))
		for k, v := range e.headers {
			hcp[k] = v
		}
		cp.headers = hcp
	}
	if e.externalDesc != nil {
		descCopy := *e.externalDesc
		cp.externalDesc = &descCopy
	}
	if e.terminalMeta != nil {
		metadataCopy := *e.terminalMeta
		cp.terminalMeta = &metadataCopy
	}
	if e.failureCauses.Len() > 0 {
		cp.failureCauses = e.failureCauses.Copy()
	}
	return cp
}

// EvidenceBatch is an immutable record of events, per-channel pending and
// committed look-behind state, staged response-start metadata, terminal flag,
// and commit state collected for a single stream epoch. Filter results are
// maintained outside the batch as a separate set of FilterOutcome values.
// All map/slice fields use recursive deep-copy in constructors and accessors.
type EvidenceBatch struct {
	events              []NormalizedEvent
	channelPending      map[string][]NormalizedEvent
	committedLookBehind map[string][]NormalizedEvent
	stagedStart         *ResponseStart
	terminalFlag        bool
	commitState         CommitState
	capturedAt          time.Time
}

// validateEvidenceBatchLifecycle enforces terminal evaluation and commit
// state invariants shared between the constructor and Validate.
//
// terminal_committed is a terminal lifecycle state: a new EvidenceBatch may
// never be formed from it.
//
// When terminalFlag is true, the batch is a final evaluation batch. It must
// contain exactly one terminal or provider-error event and may only be
// created in transport_uncommitted or stream_open. A valid staged
// response-start is preserved in transport_uncommitted but forbidden in
// stream_open.
//
// When terminalFlag is false, the batch must not contain terminal or
// provider-error events and must not be in terminal_committed state.
func validateEvidenceBatchLifecycle(
	events []NormalizedEvent,
	stagedStart *ResponseStart,
	terminalFlag bool,
	commitState CommitState,
) error {
	if commitState == CommitStateTerminalCommitted {
		return errors.New("streamgate: evidence batch must not be formed in terminal_committed state")
	}
	if terminalFlag {
		terminalCount := 0
		for _, ev := range events {
			if ev.kind == EventKindTerminal || ev.kind == EventKindProviderError {
				terminalCount++
			}
		}
		if terminalCount != 1 {
			return errors.New("streamgate: terminal batch must contain exactly one terminal event")
		}
	} else {
		for _, ev := range events {
			if ev.kind == EventKindTerminal || ev.kind == EventKindProviderError {
				return errors.New("streamgate: non-terminal batch must not contain terminal events")
			}
		}
	}
	if stagedStart != nil {
		if err := stagedStart.Validate(); err != nil {
			return errors.New("streamgate: evidence batch staged response start: " + err.Error())
		}
		if commitState != CommitStateTransportUncommitted {
			return errors.New("streamgate: staged response start requires transport_uncommitted state")
		}
	}
	return nil
}

// validateEvidenceTail validates per-channel pending and look-behind event
// lists. Each entry must have a non-empty channel, a valid nested event,
// Channel() matching the map key, and must not be a terminal event.
func validateEvidenceTail(channel string, events []NormalizedEvent) error {
	if channel == "" {
		return errors.New("streamgate: evidence tail channel key must not be empty")
	}
	for i, ev := range events {
		if err := ev.Validate(); err != nil {
			return errors.New("streamgate: evidence tail event " + channel + "[" + strconv.Itoa(i) + "]: " + err.Error())
		}
		if ev.Channel() != channel {
			return errors.New("streamgate: evidence tail event " + channel + "[" + strconv.Itoa(i) + "] channel mismatch")
		}
		if ev.Kind() == EventKindTerminal || ev.Kind() == EventKindProviderError {
			return errors.New("streamgate: evidence tail event " + channel + "[" + strconv.Itoa(i) + "] must not be terminal")
		}
	}
	return nil
}

// NewEvidenceBatch creates an EvidenceBatch with recursive deep copies of
// mutable inputs. The returned value is immutable to callers.
func NewEvidenceBatch(
	events []NormalizedEvent,
	channelPending map[string][]NormalizedEvent,
	committedLookBehind map[string][]NormalizedEvent,
	stagedStart *ResponseStart,
	terminalFlag bool,
	commitState CommitState,
	capturedAt time.Time,
) (EvidenceBatch, error) {
	if capturedAt.IsZero() {
		return EvidenceBatch{}, errors.New("streamgate: evidence batch captured timestamp is required")
	}
	if err := commitState.Validate(); err != nil {
		return EvidenceBatch{}, err
	}

	// Validate terminal/commit/staged-start combinations via shared helper.
	if err := validateEvidenceBatchLifecycle(events, stagedStart, terminalFlag, commitState); err != nil {
		return EvidenceBatch{}, err
	}

	// Validate all events and build deep copies.
	cpEvents := make([]NormalizedEvent, len(events))
	for i, ev := range events {
		if err := ev.Validate(); err != nil {
			return EvidenceBatch{}, errors.New("streamgate: evidence batch event " + strconv.Itoa(i) + ": " + err.Error())
		}
		cpEvents[i] = cloneNormalizedEvent(ev)
	}

	// Validate and deep-copy per-channel pending events.
	cpPending := make(map[string][]NormalizedEvent, len(channelPending))
	for k, v := range channelPending {
		if err := validateEvidenceTail(k, v); err != nil {
			return EvidenceBatch{}, err
		}
		cp := make([]NormalizedEvent, len(v))
		for j, ev := range v {
			cp[j] = cloneNormalizedEvent(ev)
		}
		cpPending[k] = cp
	}

	// Validate and deep-copy per-channel committed look-behind.
	cpLookBehind := make(map[string][]NormalizedEvent, len(committedLookBehind))
	for k, v := range committedLookBehind {
		if err := validateEvidenceTail(k, v); err != nil {
			return EvidenceBatch{}, err
		}
		cp := make([]NormalizedEvent, len(v))
		for j, ev := range v {
			cp[j] = cloneNormalizedEvent(ev)
		}
		cpLookBehind[k] = cp
	}

	var cpStart *ResponseStart
	if stagedStart != nil {
		cpStart = cloneResponseStart(stagedStart)
	}

	return EvidenceBatch{
		events:              cpEvents,
		channelPending:      cpPending,
		committedLookBehind: cpLookBehind,
		stagedStart:         cpStart,
		terminalFlag:        terminalFlag,
		commitState:         commitState,
		capturedAt:          capturedAt,
	}, nil
}

// Validate returns nil when the EvidenceBatch is in a consistent state.
// Uses shared lifecycle and tail validation helpers.
func (b EvidenceBatch) Validate() error {
	if b.capturedAt.IsZero() {
		return errors.New("streamgate: evidence batch captured timestamp is required")
	}
	if err := b.commitState.Validate(); err != nil {
		return err
	}
	for i, ev := range b.events {
		if err := ev.Validate(); err != nil {
			return errors.New("streamgate: evidence batch event " + strconv.Itoa(i) + ": " + err.Error())
		}
	}
	for k, v := range b.channelPending {
		if err := validateEvidenceTail(k, v); err != nil {
			return err
		}
	}
	for k, v := range b.committedLookBehind {
		if err := validateEvidenceTail(k, v); err != nil {
			return err
		}
	}
	if err := validateEvidenceBatchLifecycle(
		b.events, b.stagedStart, b.terminalFlag, b.commitState,
	); err != nil {
		return err
	}
	return nil
}

// Events returns a defensive deep copy of the batch events.
func (b EvidenceBatch) Events() []NormalizedEvent {
	if b.events == nil {
		return nil
	}
	out := make([]NormalizedEvent, len(b.events))
	for i, ev := range b.events {
		out[i] = cloneNormalizedEvent(ev)
	}
	return out
}

// ChannelPending returns a defensive deep copy of the per-channel pending events.
func (b EvidenceBatch) ChannelPending() map[string][]NormalizedEvent {
	if b.channelPending == nil {
		return nil
	}
	out := make(map[string][]NormalizedEvent, len(b.channelPending))
	for k, v := range b.channelPending {
		cp := make([]NormalizedEvent, len(v))
		for j, ev := range v {
			cp[j] = cloneNormalizedEvent(ev)
		}
		out[k] = cp
	}
	return out
}

// CommittedLookBehind returns a defensive deep copy of the per-channel committed look-behind.
func (b EvidenceBatch) CommittedLookBehind() map[string][]NormalizedEvent {
	if b.committedLookBehind == nil {
		return nil
	}
	out := make(map[string][]NormalizedEvent, len(b.committedLookBehind))
	for k, v := range b.committedLookBehind {
		cp := make([]NormalizedEvent, len(v))
		for j, ev := range v {
			cp[j] = cloneNormalizedEvent(ev)
		}
		out[k] = cp
	}
	return out
}

// StagedResponseStart returns a defensive deep copy of the staged response start.
func (b EvidenceBatch) StagedResponseStart() *ResponseStart {
	return cloneResponseStart(b.stagedStart)
}

// TerminalFlag returns whether this batch is a terminal evaluation batch.
func (b EvidenceBatch) TerminalFlag() bool { return b.terminalFlag }

// CommitState returns the commit state of this batch.
func (b EvidenceBatch) CommitState() CommitState { return b.commitState }

// CapturedAt returns the timestamp when this batch was captured.
func (b EvidenceBatch) CapturedAt() time.Time { return b.capturedAt }

// BaseDisposition returns the canonical base disposition for the evidence batch.
func (b EvidenceBatch) BaseDisposition() (BaseEventDisposition, error) {
	if err := b.Validate(); err != nil {
		return "", err
	}
	if b.terminalFlag {
		for _, ev := range b.events {
			if ev.Kind() == EventKindTerminal || ev.Kind() == EventKindProviderError {
				return ev.Disposition(), nil
			}
		}
		return "", errors.New("streamgate: terminal batch missing terminal event")
	}
	if len(b.events) > 0 {
		return b.events[len(b.events)-1].Disposition(), nil
	}
	if b.stagedStart != nil {
		return BaseDispositionHold, nil
	}
	return "", errors.New("streamgate: evidence batch has no events or staged response start")
}
