package streamgate

import (
	"context"
	"errors"
	"sort"
	"time"
)

// FilterEvaluator is the minimal execution seam that the parallel evaluator
// consumes. Implementations are produced by the filter-registry Task and
// adapted to this seam. The seam is intentionally small: only an immutable
// identity and a synchronous, context-aware evaluation call.
type FilterEvaluator interface {
	// ID returns a stable, ASCII-only identifier for this evaluator.
	ID() string

	// Evaluate runs the filter against the given batch and returns a
	// sanitized FilterDecision or a non-nil error. Implementations must not
	// retain the batch beyond the call.
	Evaluate(context.Context, EvidenceBatch) (FilterDecision, error)
}

// FilterEnforcement identifies how a filter's decision is enforced when the
// current epoch requires evaluation. The parallel evaluator reads this value
// to decide whether a blocking violation must terminate the stream.
type FilterEnforcement string

const (
	// FilterEnforcementBlocking indicates the filter may halt release of the
	// current event downstream.
	FilterEnforcementBlocking FilterEnforcement = "blocking"

	// FilterEnforcementObserveOnly indicates the filter observes the event
	// but must not halt release even on a violation.
	FilterEnforcementObserveOnly FilterEnforcement = "observe_only"
)

// knownFilterEnforcements is the closed set of valid FilterEnforcement values.
var knownFilterEnforcements = map[FilterEnforcement]struct{}{
	FilterEnforcementBlocking:    {},
	FilterEnforcementObserveOnly: {},
}

// Validate returns nil when the FilterEnforcement is a known lifecycle value.
func (e FilterEnforcement) Validate() error {
	switch e {
	case FilterEnforcementBlocking, FilterEnforcementObserveOnly:
		return nil
	}
	return errors.New("streamgate: unknown filter enforcement: " + string(e))
}

// EpochFilter binds a FilterEvaluator to its execution contract for a single
// epoch. It encodes whether the filter applies, whether its trigger is ready,
// and how its decision is enforced. Construction validates every field and
// normalises readiness into a stable predicate that the parallel evaluator
// reads before dispatching to the guardrail.
type EpochFilter struct {
	id            StableToken
	evaluator     FilterEvaluator
	priority      int
	subscribed    bool
	triggerReady  bool
	blocksRelease bool
	enforcement   FilterEnforcement
	timeout       time.Duration
}

// NewEpochFilter creates a validated EpochFilter. All fields are enforced:
//
//   - evaluator must be non-nil.
//   - evaluator.ID() must be a valid stable token and non-empty.
//   - priority must be non-negative.
//   - enforcement must be a known value.
//   - timeout must be strictly positive.
//
// The returned value is immutable to callers.
func NewEpochFilter(
	evaluator FilterEvaluator,
	priority int,
	subscribedEventPresent, triggerReady, blocksRelease bool,
	enforcement FilterEnforcement,
	timeout time.Duration,
) (EpochFilter, error) {
	if evaluator == nil {
		return EpochFilter{}, errors.New("streamgate: epoch filter evaluator is required")
	}
	id, err := NewStableTokenRequired("filterID", evaluator.ID())
	if err != nil {
		return EpochFilter{}, errors.New("streamgate: epoch filter evaluator id: " + err.Error())
	}
	if priority < 0 {
		return EpochFilter{}, errors.New("streamgate: epoch filter priority must be nonnegative")
	}
	if err := enforcement.Validate(); err != nil {
		return EpochFilter{}, errors.New("streamgate: epoch filter enforcement: " + err.Error())
	}
	if timeout <= 0 {
		return EpochFilter{}, errors.New("streamgate: epoch filter evaluation timeout must be positive")
	}
	return EpochFilter{
		id:            id,
		evaluator:     evaluator,
		priority:      priority,
		subscribed:    subscribedEventPresent,
		triggerReady:  triggerReady,
		blocksRelease: blocksRelease,
		enforcement:   enforcement,
		timeout:       timeout,
	}, nil
}

// ID returns the stable identifier snapshot at construction time.
// The underlying evaluator may be mutated without affecting this value.
func (f EpochFilter) ID() string { return f.id.String() }

// Priority returns the effective evaluation priority at construction time.
func (f EpochFilter) Priority() int { return f.priority }

// EnforcedAs returns the enforcement mode recorded at construction.
func (f EpochFilter) EnforcedAs() FilterEnforcement { return f.enforcement }

// SubscribedEventPresent returns whether the current epoch subscribed to this
// filter's event kind.
func (f EpochFilter) SubscribedEventPresent() bool { return f.subscribed }

// TriggerReady returns whether the filter's trigger condition is satisfied
// for the current epoch.
func (f EpochFilter) TriggerReady() bool { return f.triggerReady }

// BlocksRelease returns whether this filter blocks release of the current
// event on a violation.
func (f EpochFilter) BlocksRelease() bool { return f.blocksRelease }

// EvaluationTimeout returns the positive duration allocated to a single
// Evaluate call.
func (f EpochFilter) EvaluationTimeout() time.Duration { return f.timeout }

// EvaluatedForEpoch reports whether this filter is a ready evaluation target
// for the current epoch. The normalisation rules are:
//
//   - unsubscribed epochs are not_applicable_for_epoch.
//   - subscribed epochs where the trigger is not ready and the filter blocks
//     release are deferred_by_requirement.
//   - subscribed epochs where the trigger is not ready and the filter does
//     not block release are not_applicable_for_epoch.
//   - subscribed epochs where the trigger is ready are ready for evaluation.
//
// The parallel evaluator dispatches to the guardrail only when this returns
// true.
func (f EpochFilter) EvaluatedForEpoch() bool {
	if !f.subscribed {
		return false
	}
	if !f.triggerReady {
		if f.blocksRelease {
			// deferred_by_requirement — not evaluated this epoch.
			return false
		}
		// nonblocking trigger unready — treated as not_applicable.
		return false
	}
	return true
}

// NormalizeOutcome derives the canonical FilterOutcome for the current epoch
// from the binding state. The returned outcome captures exactly one of
// evaluated, evaluation_error, not_applicable_for_epoch, or
// deferred_by_requirement with no decision and no raw error payload.
func (f EpochFilter) NormalizeOutcome() FilterOutcome {
	if !f.subscribed {
		return NewFilterOutcomeNotApplicableForEpoch()
	}
	if !f.triggerReady {
		if f.blocksRelease {
			return NewFilterOutcomeDeferredByRequirement()
		}
		return NewFilterOutcomeNotApplicableForEpoch()
	}
	// Ready paths do not produce a pre-evaluation outcome; the evaluator
	// result will be wrapped separately by the parallel coordinator.
	return FilterOutcome{}
}

// Evaluator returns the bound FilterEvaluator for direct invocation. The
// parallel coordinator uses this seam to fan out to the guardrail.
func (f EpochFilter) Evaluator() FilterEvaluator { return f.evaluator }

// EvaluationFailureDisposition identifies how an evaluation error is
// propagated by the parallel coordinator. It is derived from the filter's
// enforcement mode and whether the outcome was an actual evaluation error.
type EvaluationFailureDisposition string

const (
	// EvaluationFailureDispositionNone indicates no evaluation error
	// occurred or the outcome was a successful evaluated decision.
	EvaluationFailureDispositionNone EvaluationFailureDisposition = "none"

	// EvaluationFailureDispositionBlockingFatal indicates a blocking
	// filter produced a violation or fatal decision. The coordinator
	// must terminate the stream.
	EvaluationFailureDispositionBlockingFatal EvaluationFailureDisposition = "blocking_fatal"

	// EvaluationFailureDispositionObserveError indicates an observe-only
	// filter produced a violation or fatal decision. The coordinator
	// logs the error code but does not terminate the stream.
	EvaluationFailureDispositionObserveError EvaluationFailureDisposition = "observe_error"
)

// knownEvaluationFailureDispositions is the closed set of valid
// EvaluationFailureDisposition values.
var knownEvaluationFailureDispositions = map[EvaluationFailureDisposition]struct{}{
	EvaluationFailureDispositionNone:          {},
	EvaluationFailureDispositionBlockingFatal: {},
	EvaluationFailureDispositionObserveError:  {},
}

// Validate returns nil when the EvaluationFailureDisposition is a known
// lifecycle value.
func (d EvaluationFailureDisposition) Validate() error {
	switch d {
	case EvaluationFailureDispositionNone,
		EvaluationFailureDispositionBlockingFatal,
		EvaluationFailureDispositionObserveError:
		return nil
	}
	return errors.New("streamgate: unknown evaluation failure disposition: " + string(d))
}

// EpochFilterOutcome records a filter evaluation result for the immutable
// EvaluationSet. Raw errors are not preserved; the stable filter id, canonical
// FilterOutcome, enforcement mode, and failure disposition are retained. This
// guarantees that downstream Arbiter inputs cannot depend on completion order
// or leak raw error details.
type EpochFilterOutcome struct {
	filterID           StableToken
	priority           int
	outcome            FilterOutcome
	enforcement        FilterEnforcement
	failureDisposition EvaluationFailureDisposition
}

// EvaluationSet is an immutable, stable-ordered collection of
// EpochFilterOutcome entries. Every accessor returns a defensive copy so
// that caller mutation cannot alter the set. The set is sorted by filter
// id in ascending stable-token order at construction time and remains
// stable regardless of completion order. The raw error payload is never
// preserved; only the stable error code survives in the outcome kind.
type EvaluationSet struct {
	outcomes []EpochFilterOutcome
}

// NewEpochFilterOutcome constructs a validated EpochFilterOutcome.
//
// The caller provides the filter id, priority, the raw FilterOutcome, and the
// enforcement mode. The constructor derives the failure disposition from
// these inputs:
//
//   - evaluated + blocking violation/fatal → blocking_fatal
//   - evaluated + observe_only violation/fatal → observe_error
//   - evaluation_error + blocking → blocking_fatal
//   - evaluation_error + observe_only → observe_error
//   - not_applicable / deferred / evaluated with pass/observe/replacement → none
//
// The constructor rejects:
//   - empty filter id
//   - unknown filter id grammar
//   - negative priority
//   - unknown enforcement
//   - FilterOutcome that fails its own Validate()
//
// The returned value is immutable to callers.
func NewEpochFilterOutcome(
	filterID string,
	priority int,
	outcome FilterOutcome,
	enforcement FilterEnforcement,
) (EpochFilterOutcome, error) {
	if filterID == "" {
		return EpochFilterOutcome{}, errors.New("streamgate: epoch filter outcome filter id is required")
	}
	fid, err := NewStableTokenRequired("filterID", filterID)
	if err != nil {
		return EpochFilterOutcome{}, errors.New("streamgate: epoch filter outcome filter id: " + err.Error())
	}
	if priority < 0 {
		return EpochFilterOutcome{}, errors.New("streamgate: epoch filter outcome priority must be nonnegative")
	}
	if err := enforcement.Validate(); err != nil {
		return EpochFilterOutcome{}, errors.New("streamgate: epoch filter outcome enforcement: " + err.Error())
	}
	if err := outcome.Validate(); err != nil {
		return EpochFilterOutcome{}, errors.New("streamgate: epoch filter outcome: " + err.Error())
	}

	// Enforce that an evaluated outcome's decision filter id matches the
	// wrapper's filter id. This prevents package-local forging from decoupling
	// the outcome from its wrapper identity.
	if outcome.Kind() == FilterOutcomeKindEvaluated && outcome.Decision() != nil {
		if outcome.Decision().FilterID() != fid.String() {
			return EpochFilterOutcome{}, errors.New("streamgate: evaluated outcome filter id mismatch")
		}
	}

	disposition, err := deriveEvaluationFailureDisposition(outcome, enforcement)
	if err != nil {
		return EpochFilterOutcome{}, err
	}

	return EpochFilterOutcome{
		filterID:           fid,
		priority:           priority,
		outcome:            outcome,
		enforcement:        enforcement,
		failureDisposition: disposition,
	}, nil
}

// FilterID returns the stable filter identifier.
func (o EpochFilterOutcome) FilterID() string { return o.filterID.value }

// Priority returns the effective evaluation priority at construction time.
func (o EpochFilterOutcome) Priority() int { return o.priority }

// Outcome returns a defensive copy of the canonical FilterOutcome.
func (o EpochFilterOutcome) Outcome() FilterOutcome {
	cp := o.outcome
	return cp
}

// Enforcement returns the enforcement mode at construction time.
func (o EpochFilterOutcome) Enforcement() FilterEnforcement { return o.enforcement }

// FailureDisposition returns the derived failure disposition.
func (o EpochFilterOutcome) FailureDisposition() EvaluationFailureDisposition {
	return o.failureDisposition
}

// NewEvaluationSet constructs an immutable, stable-ordered EvaluationSet from
// the provided EpochFilterOutcome entries. The constructor:
//
//   - Validates every entry via EpochFilterOutcome.Validate().
//   - Rejects duplicate filter ids.
//   - Rejects entries whose outcome kind is evaluated but whose decision is
//     nil (the evaluator must produce a valid FilterDecision).
//   - Rejects entries whose outcome kind is evaluation_error but whose error
//     code is empty.
//   - Sorts by filter id in ascending stable-token order.
//   - Returns defensive copies so that caller mutation cannot alter the set.
//
// The returned value's accessor methods also return defensive copies.
func NewEvaluationSet(outcomes []EpochFilterOutcome) (EvaluationSet, error) {
	if len(outcomes) == 0 {
		return EvaluationSet{}, nil
	}

	seen := make(map[string]struct{}, len(outcomes))
	cp := make([]EpochFilterOutcome, len(outcomes))
	for i, o := range outcomes {
		if err := o.Validate(); err != nil {
			return EvaluationSet{}, errors.New("streamgate: evaluation set entry " + string(rune('0'+i)) + ": " + err.Error())
		}
		if _, dup := seen[o.filterID.value]; dup {
			return EvaluationSet{}, errors.New("streamgate: evaluation set duplicate filter id: " + o.filterID.value)
		}
		seen[o.filterID.value] = struct{}{}
		cp[i] = o
	}

	// Stable ascending order by filter id.
	sort.SliceStable(cp, func(i, j int) bool {
		return cp[i].filterID.value < cp[j].filterID.value
	})

	return EvaluationSet{outcomes: cp}, nil
}

// Validate returns nil when the EvaluationSet is in a consistent state.
func (s EvaluationSet) Validate() error {
	seen := make(map[string]struct{}, len(s.outcomes))
	for _, o := range s.outcomes {
		if err := o.Validate(); err != nil {
			return err
		}
		if _, dup := seen[o.filterID.value]; dup {
			return errors.New("streamgate: evaluation set duplicate filter id: " + o.filterID.value)
		}
		seen[o.filterID.value] = struct{}{}
	}
	return nil
}

// Len returns the number of outcomes in the set.
func (s EvaluationSet) Len() int { return len(s.outcomes) }

// At returns a defensive copy of the outcome at index i. Callers must not
// mutate the returned value.
func (s EvaluationSet) At(i int) (EpochFilterOutcome, error) {
	if i < 0 || i >= len(s.outcomes) {
		return EpochFilterOutcome{}, errors.New("streamgate: evaluation set index out of range")
	}
	return s.outcomes[i], nil
}

// ByID returns a defensive copy of the outcome whose filter id matches the
// given id, or an error if no such entry exists.
func (s EvaluationSet) ByID(id string) (EpochFilterOutcome, error) {
	for _, o := range s.outcomes {
		if o.filterID.value == id {
			return o, nil
		}
	}
	return EpochFilterOutcome{}, errors.New("streamgate: evaluation set filter id not found: " + id)
}

// All returns a defensive copy of all outcomes in stable id order.
func (s EvaluationSet) All() []EpochFilterOutcome {
	if s.outcomes == nil {
		return nil
	}
	cp := make([]EpochFilterOutcome, len(s.outcomes))
	copy(cp, s.outcomes)
	return cp
}

// Validate returns nil when the EpochFilterOutcome is in a consistent state.
func (o EpochFilterOutcome) Validate() error {
	if o.filterID.value == "" {
		return errors.New("streamgate: epoch filter outcome filter id is required")
	}
	if err := validateStableTokenRequired("filterID", o.filterID.value); err != nil {
		return err
	}
	if o.priority < 0 {
		return errors.New("streamgate: epoch filter outcome priority must be nonnegative")
	}
	if err := o.outcome.Validate(); err != nil {
		return err
	}
	if err := o.enforcement.Validate(); err != nil {
		return err
	}
	if err := o.failureDisposition.Validate(); err != nil {
		return err
	}
	switch o.outcome.Kind() {
	case FilterOutcomeKindEvaluated:
		if o.outcome.Decision() == nil {
			return errors.New("streamgate: evaluated outcome must carry a decision")
		}
		if o.outcome.Decision().FilterID() != o.filterID.value {
			return errors.New("streamgate: evaluated outcome filter id mismatch")
		}
	case FilterOutcomeKindEvaluationError:
		if o.outcome.ErrorCode() == "" {
			return errors.New("streamgate: evaluation_error outcome must carry an error code")
		}
	case FilterOutcomeKindNotApplicableForEpoch, FilterOutcomeKindDeferredByRequirement:
		// These are always valid; nothing to check beyond Validate.
	default:
		return errors.New("streamgate: unknown outcome kind")
	}
	expected, err := deriveEvaluationFailureDisposition(o.outcome, o.enforcement)
	if err != nil {
		return err
	}
	if o.failureDisposition != expected {
		return errors.New("streamgate: epoch filter outcome failure disposition mismatch")
	}
	return nil
}

// deriveEvaluationFailureDisposition derives the canonical failure disposition
// from the given outcome and enforcement mode. The rules are the single
// source of truth shared between NewEpochFilterOutcome and
// EpochFilterOutcome.Validate():
//
//   - evaluated + blocking violation/fatal → blocking_fatal
//   - evaluated + observe_only violation/fatal → observe_error
//   - evaluation_error + blocking → blocking_fatal
//   - evaluation_error + observe_only → observe_error
//   - not_applicable / deferred / evaluated with pass/observe/replacement → none
//
// Returns an error only for outcomes whose kind is not in the known set.
func deriveEvaluationFailureDisposition(
	outcome FilterOutcome,
	enforcement FilterEnforcement,
) (EvaluationFailureDisposition, error) {
	switch outcome.Kind() {
	case FilterOutcomeKindEvaluated:
		decision := outcome.Decision()
		if decision == nil {
			return "", errors.New("streamgate: evaluated outcome must carry a decision")
		}
		switch decision.Kind() {
		case FilterDecisionKindPass, FilterDecisionKindObserve, FilterDecisionKindReplacement:
			return EvaluationFailureDispositionNone, nil
		case FilterDecisionKindViolation, FilterDecisionKindFatal:
			if enforcement == FilterEnforcementBlocking {
				return EvaluationFailureDispositionBlockingFatal, nil
			}
			return EvaluationFailureDispositionObserveError, nil
		default:
			return EvaluationFailureDispositionNone, nil
		}
	case FilterOutcomeKindEvaluationError:
		if enforcement == FilterEnforcementBlocking {
			return EvaluationFailureDispositionBlockingFatal, nil
		}
		return EvaluationFailureDispositionObserveError, nil
	case FilterOutcomeKindNotApplicableForEpoch, FilterOutcomeKindDeferredByRequirement:
		return EvaluationFailureDispositionNone, nil
	default:
		return "", errors.New("streamgate: epoch filter outcome unknown kind")
	}
}
