package streamgate

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

// FilterContext provides read-only access to the configuration, request, and
// execution metadata that determines whether a filter applies. All field
// accessors return defensive copies so that caller mutation cannot alter the
// snapshot. FilterContext is immutable to callers after construction.
type FilterContext struct {
	configGeneration string
	attemptID        string
	epochID          *uint64

	environment string

	endpoint string
	family   string

	modelGroup     string
	actualModel    string
	actualProvider string
	executionPath  string

	streamID          string
	commitState       CommitState
	hasToolSideEffect bool
	terminal          bool

	stableCorrelation string
}

// FilterContextBuilder constructs a FilterContext step-by-step. The builder
// defers final validation until Build() so callers can accumulate partial
// state without constructing an invalid intermediate context.
type FilterContextBuilder struct {
	configGeneration string
	attemptID        string
	epochID          *uint64

	environment string

	endpoint string
	family   string

	modelGroup     string
	actualModel    string
	actualProvider string
	executionPath  string

	streamID          string
	commitState       CommitState
	hasToolSideEffect bool
	terminal          bool

	stableCorrelation string
}

// NewFilterContextBuilder returns a builder with the config generation and
// attempt id already populated. Epoch is unset by default.
func NewFilterContextBuilder(configGeneration, attemptID string) *FilterContextBuilder {
	return &FilterContextBuilder{
		configGeneration: configGeneration,
		attemptID:        attemptID,
	}
}

// SetEpoch binds an epoch id to the builder. Epochs are validated as
// strictly positive. SetEpoch returns a new builder so the original is
// effectively immutable — the registry uses this to produce epoch-bound
// copies without mutating the base attempt context.
func (b *FilterContextBuilder) SetEpoch(epochID uint64) (*FilterContextBuilder, error) {
	if epochID == 0 {
		return nil, errors.New("streamgate: filter context builder epoch must be strictly positive")
	}
	cp := *b
	cp.epochID = &epochID
	return &cp, nil
}

// ClearEpoch returns a builder with epoch removed. The returned builder
// shares all other fields with the receiver. This is used when the registry
// constructs the attempt-level base context before epoch binding.
func (b *FilterContextBuilder) ClearEpoch() *FilterContextBuilder {
	cp := *b
	cp.epochID = nil
	return &cp
}

// SetEnvironment binds the environment label.
func (b *FilterContextBuilder) SetEnvironment(env string) *FilterContextBuilder {
	cp := *b
	cp.environment = env
	return &cp
}

// SetEndpoint binds the endpoint label.
func (b *FilterContextBuilder) SetEndpoint(ep string) *FilterContextBuilder {
	cp := *b
	cp.endpoint = ep
	return &cp
}

// SetFamily binds the family label.
func (b *FilterContextBuilder) SetFamily(f string) *FilterContextBuilder {
	cp := *b
	cp.family = f
	return &cp
}

// SetModelGroup binds the model group label.
func (b *FilterContextBuilder) SetModelGroup(g string) *FilterContextBuilder {
	cp := *b
	cp.modelGroup = g
	return &cp
}

// SetActualModel binds the actual resolved model name.
func (b *FilterContextBuilder) SetActualModel(m string) *FilterContextBuilder {
	cp := *b
	cp.actualModel = m
	return &cp
}

// SetActualProvider binds the actual resolved provider.
func (b *FilterContextBuilder) SetActualProvider(p string) *FilterContextBuilder {
	cp := *b
	cp.actualProvider = p
	return &cp
}

// SetExecutionPath binds the execution path (e.g. primary, failover).
func (b *FilterContextBuilder) SetExecutionPath(ep string) *FilterContextBuilder {
	cp := *b
	cp.executionPath = ep
	return &cp
}

// SetStream binds the stream identifier.
func (b *FilterContextBuilder) SetStream(s string) *FilterContextBuilder {
	cp := *b
	cp.streamID = s
	return &cp
}

// SetCommitState binds the current commit state.
func (b *FilterContextBuilder) SetCommitState(cs CommitState) *FilterContextBuilder {
	cp := *b
	cp.commitState = cs
	return &cp
}

// SetHasToolSideEffect marks whether the attempt has a pending tool-side
// effect.
func (b *FilterContextBuilder) SetHasToolSideEffect(v bool) *FilterContextBuilder {
	cp := *b
	cp.hasToolSideEffect = v
	return &cp
}

// SetTerminal marks whether the attempt has reached the terminal event.
func (b *FilterContextBuilder) SetTerminal(v bool) *FilterContextBuilder {
	cp := *b
	cp.terminal = v
	return &cp
}

// SetStableCorrelation binds the stable correlation reference.
func (b *FilterContextBuilder) SetStableCorrelation(ref string) *FilterContextBuilder {
	cp := *b
	cp.stableCorrelation = ref
	return &cp
}

// Build validates required fields and returns an immutable FilterContext.
func (b *FilterContextBuilder) Build() (FilterContext, error) {
	if b.configGeneration == "" {
		return FilterContext{}, errors.New("streamgate: filter context config generation is required")
	}
	if b.attemptID == "" {
		return FilterContext{}, errors.New("streamgate: filter context attempt id is required")
	}
	return FilterContext{
		configGeneration:  b.configGeneration,
		attemptID:         b.attemptID,
		epochID:           b.epochID,
		environment:       b.environment,
		endpoint:          b.endpoint,
		family:            b.family,
		modelGroup:        b.modelGroup,
		actualModel:       b.actualModel,
		actualProvider:    b.actualProvider,
		executionPath:     b.executionPath,
		streamID:          b.streamID,
		commitState:       b.commitState,
		hasToolSideEffect: b.hasToolSideEffect,
		terminal:          b.terminal,
		stableCorrelation: b.stableCorrelation,
	}, nil
}

// ConfigGeneration returns the config generation snapshot associated with
// this context.
func (c FilterContext) ConfigGeneration() string { return c.configGeneration }

// AttemptID returns the attempt identifier.
func (c FilterContext) AttemptID() string { return c.attemptID }

// EpochID returns the epoch id, or 0 if the context is not epoch-bound.
func (c FilterContext) EpochID() uint64 {
	if c.epochID == nil {
		return 0
	}
	return *c.epochID
}

// HasEpoch returns true when the context carries a positive epoch id.
func (c FilterContext) HasEpoch() bool { return c.epochID != nil }

// Environment returns the runtime environment label.
func (c FilterContext) Environment() string { return c.environment }

// Endpoint returns the transport endpoint label.
func (c FilterContext) Endpoint() string { return c.endpoint }

// Family returns the model family label.
func (c FilterContext) Family() string { return c.family }

// ModelGroup returns the model group the actual model was resolved from.
func (c FilterContext) ModelGroup() string { return c.modelGroup }

// ActualModel returns the resolved model name.
func (c FilterContext) ActualModel() string { return c.actualModel }

// ActualProvider returns the resolved provider.
func (c FilterContext) ActualProvider() string { return c.actualProvider }

// ExecutionPath returns the execution path (e.g. primary, failover).
func (c FilterContext) ExecutionPath() string { return c.executionPath }

// StreamID returns the stream identifier.
func (c FilterContext) StreamID() string { return c.streamID }

// CommitState returns the current commit state.
func (c FilterContext) CommitState() CommitState { return c.commitState }

// HasToolSideEffect returns whether the attempt has a pending tool-side effect.
func (c FilterContext) HasToolSideEffect() bool { return c.hasToolSideEffect }

// Terminal returns whether the attempt has reached a terminal event.
func (c FilterContext) Terminal() bool { return c.terminal }

// StableCorrelation returns the stable correlation reference.
func (c FilterContext) StableCorrelation() string { return c.stableCorrelation }

// WithEpoch returns a copy of the context with the given epoch id bound.
// Epoch ids must be strictly positive. The receiver is not mutated.
func (c FilterContext) WithEpoch(epochID uint64) (FilterContext, error) {
	if epochID == 0 {
		return FilterContext{}, errors.New("streamgate: filter context epoch must be strictly positive")
	}
	cp := c
	cp.epochID = &epochID
	return cp, nil
}

// Filter is the full SDD contract a filter implementation must satisfy.
// Implementations must be safe for concurrent evaluation across epochs and
// must not retain EvidenceBatch beyond Evaluate.
type Filter interface {
	// ID returns a stable, ASCII-only identifier. Must not change after
	// registration.
	ID() string

	// Applies determines whether this filter targets the given context.
	Applies(FilterContext) bool

	// HoldRequirement returns the subscription and blocking contract for
	// the given context. Used by the registry preflight and the evaluator
	// readiness normalisation.
	HoldRequirement(FilterContext) FilterHoldRequirement

	// Evaluate runs the filter against the batch and returns a sanitized
	// FilterDecision or a non-nil error. Must not retain batch references.
	Evaluate(context.Context, FilterContext, EvidenceBatch) (FilterDecision, error)
}

// FilterBase provides a validated stable identifier helper. It carries no
// runtime policy (enablement, enforcement, timeout, priority) — those belong
// to FilterPolicyLayer.
type FilterBase struct {
	id StableToken
}

// NewFilterBase constructs a FilterBase after validating the id token. The
// returned base is immutable.
func NewFilterBase(id string) (FilterBase, error) {
	st, err := NewStableTokenRequired("filterID", id)
	if err != nil {
		return FilterBase{}, err
	}
	return FilterBase{id: st}, nil
}

// ID returns the stable id string snapshot.
func (b FilterBase) ID() string { return b.id.value }

// StableTokenValue returns the internal StableToken for registry matching.
func (b FilterBase) StableTokenValue() StableToken { return b.id }

// FilterPolicyLayer carries policy overrides at a single selector level
// (default, environment, model-group, model, provider). Values are copied at
// construction and returned by accessors as defensive copies.
type FilterPolicyLayer struct {
	filterID     StableToken
	selectorType PolicySelectorType
	selectorKey  string
	enabled      bool
	enforcement  FilterEnforcement
	timeout      time.Duration
	priority     int
}

// PolicySelectorType identifies the scope of a FilterPolicyLayer. The
// registry evaluates these in ascending specificity order.
type PolicySelectorType string

const (
	// PolicySelectorDefault applies when no more specific selector matches.
	PolicySelectorDefault PolicySelectorType = "default"

	// PolicySelectorEnvironment matches the runtime environment label.
	PolicySelectorEnvironment PolicySelectorType = "environment"

	// PolicySelectorModelGroup matches the resolved model group.
	PolicySelectorModelGroup PolicySelectorType = "model_group"

	// PolicySelectorModel matches the resolved model name.
	PolicySelectorModel PolicySelectorType = "model"

	// PolicySelectorProvider matches the resolved provider name.
	PolicySelectorProvider PolicySelectorType = "provider"
)

// knownPolicySelectorTypes is the closed set of valid selector types.
var knownPolicySelectorTypes = map[PolicySelectorType]struct{}{
	PolicySelectorDefault:     {},
	PolicySelectorEnvironment: {},
	PolicySelectorModelGroup:  {},
	PolicySelectorModel:       {},
	PolicySelectorProvider:    {},
}

// Validate returns nil when the selector type is known.
func (t PolicySelectorType) Validate() error {
	switch t {
	case PolicySelectorDefault, PolicySelectorEnvironment, PolicySelectorModelGroup,
		PolicySelectorModel, PolicySelectorProvider:
		return nil
	}
	return fmt.Errorf("streamgate: unknown policy selector type: %s", string(t))
}

// NewFilterPolicyLayer constructs a validated policy layer. The returned
// layer is immutable to callers.
func NewFilterPolicyLayer(
	fID string,
	st PolicySelectorType,
	key string,
	enabled bool,
	enforcement FilterEnforcement,
	timeout time.Duration,
	priority int,
) (FilterPolicyLayer, error) {
	fIDToken, err := NewStableTokenRequired("filterPolicy filterID", fID)
	if err != nil {
		return FilterPolicyLayer{}, err
	}
	if err := st.Validate(); err != nil {
		return FilterPolicyLayer{}, err
	}
	if err := enforcement.Validate(); err != nil {
		return FilterPolicyLayer{}, fmt.Errorf("streamgate: filter policy enforcement: %w", err)
	}
	if timeout <= 0 {
		return FilterPolicyLayer{}, errors.New("streamgate: filter policy timeout must be positive")
	}
	if priority < 0 {
		return FilterPolicyLayer{}, errors.New("streamgate: filter policy priority must be non-negative")
	}
	if st == PolicySelectorDefault && key != "" {
		return FilterPolicyLayer{}, errors.New("streamgate: default policy selector must have empty key")
	}
	if st != PolicySelectorDefault && key == "" {
		return FilterPolicyLayer{}, errors.New("streamgate: non-default policy selector must have non-empty key")
	}
	return FilterPolicyLayer{
		filterID:     fIDToken,
		selectorType: st,
		selectorKey:  key,
		enabled:      enabled,
		enforcement:  enforcement,
		timeout:      timeout,
		priority:     priority,
	}, nil
}

// FilterID returns the filter id this policy layer belongs to.
func (l FilterPolicyLayer) FilterID() string { return l.filterID.value }

// SelectorType returns the policy layer selector type.
func (l FilterPolicyLayer) SelectorType() PolicySelectorType { return l.selectorType }

// SelectorKey returns the policy layer selector key.
func (l FilterPolicyLayer) SelectorKey() string { return l.selectorKey }

// Enabled returns the enabled override.
func (l FilterPolicyLayer) Enabled() bool { return l.enabled }

// Enforcement returns the enforcement override.
func (l FilterPolicyLayer) Enforcement() FilterEnforcement { return l.enforcement }

// Timeout returns the evaluation timeout override.
func (l FilterPolicyLayer) Timeout() time.Duration { return l.timeout }

// Priority returns the evaluation priority override.
func (l FilterPolicyLayer) Priority() int { return l.priority }

// SelectorKeyWithDefault returns the canonical selector key. For the default
// layer the key is empty; the registry treats these as the base layer.
func (l FilterPolicyLayer) SelectorKeyWithDefault() string {
	if l.selectorType == PolicySelectorDefault {
		return ""
	}
	return l.selectorKey
}

// FilterRegistration is the registry-owned immutable snapshot of a filter and
// its runtime policy. All fields are validated at construction.
type FilterRegistration struct {
	filter         Filter
	requiredCapID  string
	enabled        bool
	enforcement    FilterEnforcement
	timeout        time.Duration
	priority       int
	registrationID StableToken
}

// NewFilterRegistration constructs a validated registration. The constructor
// rejects duplicate ids (compared against the filter's own ID), post-
// construction mutation is impossible because the FilterBase id is captured
// at registration time.
func NewFilterRegistration(
	f Filter,
	requiredCapID string,
	enabled bool,
	enforcement FilterEnforcement,
	timeout time.Duration,
	priority int,
) (FilterRegistration, error) {
	if f == nil {
		return FilterRegistration{}, errors.New("streamgate: filter registration filter is required")
	}
	fID := f.ID()
	if fID == "" {
		return FilterRegistration{}, errors.New("streamgate: filter registration filter id is required")
	}
	if requiredCapID == "" {
		return FilterRegistration{}, errors.New("streamgate: filter registration required capability id is required")
	}
	if err := enforcement.Validate(); err != nil {
		return FilterRegistration{}, fmt.Errorf("streamgate: filter registration enforcement: %w", err)
	}
	if timeout <= 0 {
		return FilterRegistration{}, errors.New("streamgate: filter registration timeout must be positive")
	}
	if priority < 0 {
		return FilterRegistration{}, errors.New("streamgate: filter registration priority must be non-negative")
	}
	st, err := NewStableTokenRequired("filterRegistration", fID)
	if err != nil {
		return FilterRegistration{}, fmt.Errorf("streamgate: filter registration id: %w", err)
	}
	return FilterRegistration{
		filter:         f,
		requiredCapID:  requiredCapID,
		enabled:        enabled,
		enforcement:    enforcement,
		timeout:        timeout,
		priority:       priority,
		registrationID: st,
	}, nil
}

// Filter returns the registered filter.
func (r FilterRegistration) Filter() Filter { return r.filter }

// FilterID returns the stable filter id captured at registration time.
func (r FilterRegistration) FilterID() string { return r.registrationID.value }

// RegistrationID returns the registration token.
func (r FilterRegistration) RegistrationID() StableToken { return r.registrationID }

// RequiredCapabilityID returns the stable capability id this filter requires.
func (r FilterRegistration) RequiredCapabilityID() string { return r.requiredCapID }

// Enabled returns the default enabled override.
func (r FilterRegistration) Enabled() bool { return r.enabled }

// Enforcement returns the default enforcement override.
func (r FilterRegistration) Enforcement() FilterEnforcement { return r.enforcement }

// Timeout returns the default evaluation timeout override.
func (r FilterRegistration) Timeout() time.Duration { return r.timeout }

// Priority returns the default evaluation priority override.
func (r FilterRegistration) Priority() int { return r.priority }

// AttemptTarget identifies a candidate model/provider/execution-path tuple
// for which the registry resolves active filters.
type AttemptTarget struct {
	modelGroup    string
	model         string
	provider      string
	executionPath string
	capabilities  []string
}

// NewAttemptTarget constructs a validated AttemptTarget. The candidate
// capabilities set is captured by value so caller mutation after construction
// does not affect the registry's resolution.
func NewAttemptTarget(
	modelGroup, model, provider, executionPath string,
	capabilities []string,
) (AttemptTarget, error) {
	if model == "" {
		return AttemptTarget{}, errors.New("streamgate: attempt target model is required")
	}
	if provider == "" {
		return AttemptTarget{}, errors.New("streamgate: attempt target provider is required")
	}
	cp := make([]string, len(capabilities))
	copy(cp, capabilities)
	return AttemptTarget{
		modelGroup:    modelGroup,
		model:         model,
		provider:      provider,
		executionPath: executionPath,
		capabilities:  cp,
	}, nil
}

// ModelGroup returns the candidate model group.
func (t AttemptTarget) ModelGroup() string { return t.modelGroup }

// Model returns the candidate model.
func (t AttemptTarget) Model() string { return t.model }

// Provider returns the candidate provider.
func (t AttemptTarget) Provider() string { return t.provider }

// ExecutionPath returns the candidate execution path.
func (t AttemptTarget) ExecutionPath() string { return t.executionPath }

// Capabilities returns a defensive copy of the candidate capability set.
func (t AttemptTarget) Capabilities() []string {
	if t.capabilities == nil {
		return nil
	}
	cp := make([]string, len(t.capabilities))
	copy(cp, t.capabilities)
	return cp
}

// HasCapability returns true when the candidate capability set includes the
// given capability id.
func (t AttemptTarget) HasCapability(cap string) bool {
	for _, c := range t.capabilities {
		if c == cap {
			return true
		}
	}
	return false
}

// resolvedPolicy holds the effective policy values for a single filter after
// all matching selectors have been applied.
type resolvedPolicy struct {
	enabled     bool
	enforcement FilterEnforcement
	timeout     time.Duration
	priority    int
}

// ResolvedFilter is the registry's resolved candidate: the filter reference
// bound to its effective policy values and the attempt-level FilterContext
// snapshot at the time of resolution. The effective values are snapshot and
// immutable to filter runtime mutation.
type ResolvedFilter struct {
	reg         FilterRegistration
	baseCtx     FilterContext
	enabled     bool
	enforcement FilterEnforcement
	timeout     time.Duration
	priority    int
	requirement FilterHoldRequirement
}

// Registration returns the registration snapshot this filter was resolved from.
func (r ResolvedFilter) Registration() FilterRegistration { return r.reg }

// Filter returns the underlying filter implementation reference.
func (r ResolvedFilter) Filter() Filter { return r.reg.filter }

// FilterContextForBindEpoch returns the attempt-level base context this
// resolved filter was derived from. Tests use this to inspect the context
// that BindEpoch will extend with an epoch id.
func (r ResolvedFilter) FilterContextForBindEpoch() FilterContext { return r.baseCtx }

// FilterID returns the stable id captured at registration time. This value
// does not change even if the underlying filter's ID() is mutated after
// resolution.
func (r ResolvedFilter) FilterID() string { return r.reg.RegistrationID().String() }

// Enabled returns the effective enabled value at resolution time.
func (r ResolvedFilter) Enabled() bool { return r.enabled }

// Enforcement returns the effective enforcement at resolution time.
func (r ResolvedFilter) Enforcement() FilterEnforcement { return r.enforcement }

// Timeout returns the effective evaluation timeout at resolution time.
func (r ResolvedFilter) Timeout() time.Duration { return r.timeout }

// Priority returns the effective evaluation priority at resolution time.
func (r ResolvedFilter) Priority() int { return r.priority }

// HoldRequirement returns the snapshot of the filter's hold requirement at
// resolution time.
func (r ResolvedFilter) HoldRequirement() FilterHoldRequirement { return r.requirement }

// BindEpoch creates a private EpochFilter adapter that wraps the resolved
// filter. The epoch id is validated as strictly positive. The returned
// EpochFilter's Evaluate(ctx, batch) delegates to the underlying Filter's
// three-argument Evaluate with a FilterContext snapshot derived from the
// base context captured at resolution, with the given epoch id bound.
// Static blocksRelease and priority are derived from the registry snapshot.
func (r ResolvedFilter) BindEpoch(epochID uint64, app FilterApplicability) (EpochFilter, error) {
	if epochID == 0 {
		return EpochFilter{}, errors.New("streamgate: resolved filter epoch must be strictly positive")
	}

	if err := checkRegistrationIdentity(r.reg); err != nil {
		return EpochFilter{}, fmt.Errorf(
			"streamgate: resolved filter bind epoch: %w", err,
		)
	}

	if r.FilterID() != app.FilterID() {
		return EpochFilter{}, fmt.Errorf(
			"streamgate: applicability filter id %q does not match resolved filter id %q",
			app.FilterID(), r.FilterID(),
		)
	}

	// Core derives blocksRelease from effective enforcement and requirement mode.
	blocksRelease := (r.enforcement == FilterEnforcementBlocking && r.requirement.Mode() != FilterHoldModeNone)

	adapter := &resolvedFilterAdapter{
		resolved:      r,
		epochID:       epochID,
		subscribed:    app.SubscribedEventPresent(),
		triggerReady:  app.TriggerReady(),
		blocksRelease: blocksRelease,
	}

	return NewEpochFilter(adapter, r.priority, app.SubscribedEventPresent(), app.TriggerReady(), blocksRelease, r.enforcement, r.timeout)
}

// resolvedFilterAdapter adapts a Filter's three-argument Evaluate signature
// to the two-argument FilterEvaluator seam consumed by EpochFilter. It
// carries the immutable FilterContext snapshot captured at resolve time and
// binds the given epoch id before each evaluation.
type resolvedFilterAdapter struct {
	resolved      ResolvedFilter
	epochID       uint64
	subscribed    bool
	triggerReady  bool
	blocksRelease bool
}

func (a *resolvedFilterAdapter) ID() string { return a.resolved.reg.RegistrationID().String() }

func (a *resolvedFilterAdapter) Evaluate(
	ctx context.Context,
	batch EvidenceBatch,
) (FilterDecision, error) {
	epochCtx, err := a.resolved.baseCtx.WithEpoch(a.epochID)
	if err != nil {
		return FilterDecision{}, fmt.Errorf("streamgate: resolved filter adapter epoch context: %w", err)
	}
	return a.resolved.reg.filter.Evaluate(ctx, epochCtx, batch)
}

// RequestFilterContext is the caller-provided input at the beginning of a
// request. It captures attempt-level metadata without binding an epoch (the
// epoch is bound later via BindEpoch).
type RequestFilterContext struct {
	configGeneration  string
	attemptID         string
	environment       string
	endpoint          string
	family            string
	streamID          string
	commitState       CommitState
	hasToolSideEffect bool
	terminal          bool
	stableCorrelation string
}

// NewRequestFilterContext constructs a validated RequestFilterContext.
func NewRequestFilterContext(
	configGeneration, attemptID, environment, endpoint, family, streamID string,
	commitState CommitState,
	hasToolSideEffect bool,
	terminal bool,
	stableCorrelation string,
) (RequestFilterContext, error) {
	if configGeneration == "" {
		return RequestFilterContext{}, errors.New("streamgate: request filter context config generation is required")
	}
	if attemptID == "" {
		return RequestFilterContext{}, errors.New("streamgate: request filter context attempt id is required")
	}
	if err := commitState.Validate(); err != nil {
		return RequestFilterContext{}, fmt.Errorf("streamgate: request filter context commit state: %w", err)
	}
	return RequestFilterContext{
		configGeneration:  configGeneration,
		attemptID:         attemptID,
		environment:       environment,
		endpoint:          endpoint,
		family:            family,
		streamID:          streamID,
		commitState:       commitState,
		hasToolSideEffect: hasToolSideEffect,
		terminal:          terminal,
		stableCorrelation: stableCorrelation,
	}, nil
}

// ConfigGeneration returns the request's config generation.
func (c RequestFilterContext) ConfigGeneration() string { return c.configGeneration }

// AttemptID returns the request's attempt id.
func (c RequestFilterContext) AttemptID() string { return c.attemptID }

// Environment returns the request's environment label.
func (c RequestFilterContext) Environment() string { return c.environment }

// Endpoint returns the request's endpoint label.
func (c RequestFilterContext) Endpoint() string { return c.endpoint }

// Family returns the request's family label.
func (c RequestFilterContext) Family() string { return c.family }

// StreamID returns the request's stream id.
func (c RequestFilterContext) StreamID() string { return c.streamID }

// CommitState returns the request's commit state.
func (c RequestFilterContext) CommitState() CommitState { return c.commitState }

// HasToolSideEffect returns whether the request has a tool-side effect.
func (c RequestFilterContext) HasToolSideEffect() bool { return c.hasToolSideEffect }

// Terminal returns whether the request has reached the terminal event.
func (c RequestFilterContext) Terminal() bool { return c.terminal }

// StableCorrelation returns the request's stable correlation.
func (c RequestFilterContext) StableCorrelation() string { return c.stableCorrelation }

// buildFilterContext produces a FilterContext from the request context with
// no epoch binding.
func (c RequestFilterContext) buildFilterContext() (FilterContext, error) {
	bc := NewFilterContextBuilder(c.configGeneration, c.attemptID)
	bc = bc.SetEnvironment(c.environment)
	bc = bc.SetEndpoint(c.endpoint)
	bc = bc.SetFamily(c.family)
	bc = bc.SetStream(c.streamID)
	bc = bc.SetCommitState(c.commitState)
	bc = bc.SetHasToolSideEffect(c.hasToolSideEffect)
	bc = bc.SetTerminal(c.terminal)
	bc = bc.SetStableCorrelation(c.stableCorrelation)
	return bc.Build()
}

// effectiveEnabledForContext resolves the effective enabled state for a
// registration at the given context using the snapshot's policy layers.
// This is a shared helper used by both BeginRequest and ResolveAttempt.
func (s *FilterRegistrySnapshot) effectiveEnabledForContext(
	reg FilterRegistration, ctx FilterContext,
) resolvedPolicy {
	effective := resolvedPolicy{
		enabled:     reg.Enabled(),
		enforcement: reg.Enforcement(),
		timeout:     reg.Timeout(),
		priority:    reg.Priority(),
	}

	regID := reg.RegistrationID().String()

	if len(s.policies) == 0 {
		return effective
	}

	sorted := make([]FilterPolicyLayer, 0)
	for _, p := range s.policies {
		if p.FilterID() != regID {
			continue
		}
		if selectorMatchesContext(p, ctx) {
			sorted = append(sorted, p)
		}
	}

	if len(sorted) == 0 {
		return effective
	}

	sort.SliceStable(sorted, func(i, j int) bool {
		si := specificityOrder[sorted[i].SelectorType()]
		sj := specificityOrder[sorted[j].SelectorType()]
		if si != sj {
			return si < sj
		}
		return sorted[i].SelectorKey() < sorted[j].SelectorKey()
	})

	last := sorted[len(sorted)-1]
	effective = resolvedPolicy{
		enabled:     last.Enabled(),
		enforcement: last.Enforcement(),
		timeout:     last.Timeout(),
		priority:    last.Priority(),
	}

	return effective
}

// RequestFilterSnapshot is the per-request immutable snapshot. Filter
// resolution is deferred to target-aware calls so that provider/model
// applicability is evaluated against the actual candidate context.
type RequestFilterSnapshot struct {
	generation string
	snap       FilterRegistrySnapshot
	baseCtx    FilterContext
}

// NewRequestFilterSnapshot creates a RequestFilterSnapshot from an existing
// FilterRegistrySnapshot and request context. It validates generation match,
// verifies registration identities, and builds the attempt-level base context.
// Filter resolution is deferred to target-aware calls
// (RequiredFilterCapabilities, EligibleCandidates, ResolveAttempt).
func NewRequestFilterSnapshot(
	snap FilterRegistrySnapshot,
	ctx RequestFilterContext,
) (RequestFilterSnapshot, error) {
	if snap.generation != ctx.ConfigGeneration() {
		return RequestFilterSnapshot{}, fmt.Errorf(
			"streamgate: request filter snapshot generation mismatch: request=%s, snapshot=%s",
			ctx.ConfigGeneration(), snap.generation,
		)
	}

	baseCtx, err := ctx.buildFilterContext()
	if err != nil {
		return RequestFilterSnapshot{}, fmt.Errorf("streamgate: request filter snapshot base context: %w", err)
	}

	if err := snap.validateRegistrationIdentity("begin request"); err != nil {
		return RequestFilterSnapshot{}, err
	}

	return RequestFilterSnapshot{
		generation: snap.generation,
		snap:       snap,
		baseCtx:    baseCtx,
	}, nil
}

// Generation returns the config generation this snapshot is bound to.
func (s RequestFilterSnapshot) Generation() string { return s.generation }

// BaseContext returns a defensive copy of the attempt-level base context.
func (s RequestFilterSnapshot) BaseContext() FilterContext { return s.baseCtx }

// RequiredFilterCapabilities returns the union of required capability ids
// for the given target. Only enabled+applicable filters at the target context
// contribute. The resolver validates registration identity, applies disabled
// early exclusion, evaluates target-aware Applies, and validates
// HoldRequirement.
func (s RequestFilterSnapshot) RequiredFilterCapabilities(target AttemptTarget) ([]string, error) {
	ctx, err := s.buildTargetContext(target)
	if err != nil {
		return nil, err
	}
	_, required, err := s.snap.resolveTargetWithCtx(ctx)
	if err != nil {
		return nil, err
	}
	return required, nil
}

// EligibleCandidates filters candidates whose capability set does not cover
// every required capability resolved at the candidate's own target context.
// Only enabled+applicable filters contribute to the required capabilities.
func (s RequestFilterSnapshot) EligibleCandidates(
	candidates []AttemptTarget,
) ([]AttemptTarget, error) {
	var eligible []AttemptTarget
	for _, candidate := range candidates {
		ctx, err := s.buildTargetContext(candidate)
		if err != nil {
			return nil, err
		}
		_, required, err := s.snap.resolveTargetWithCtx(ctx)
		if err != nil {
			return nil, err
		}
		if candidateHasAll(candidate, required) {
			eligible = append(eligible, candidate)
		}
	}
	if len(eligible) == 0 {
		return nil, ErrNoEligibleCandidate
	}
	return eligible, nil
}

// ResolveAttempt re-evaluates Applies and HoldRequirement at the target
// context and returns only enabled filters matching the target, ordered by
// stable filter id ascending.
func (s RequestFilterSnapshot) ResolveAttempt(
	target AttemptTarget,
) ([]ResolvedFilter, error) {
	ctx, err := s.buildTargetContext(target)
	if err != nil {
		return nil, err
	}
	resolved, _, err := s.snap.resolveTargetWithCtx(ctx)
	if err != nil {
		return nil, err
	}
	sort.SliceStable(resolved, func(i, j int) bool {
		return resolved[i].FilterID() < resolved[j].FilterID()
	})
	return resolved, nil
}

// buildTargetContext constructs a FilterContext from the request base context
// with model/provider/path filled in from the target.
func (s RequestFilterSnapshot) buildTargetContext(target AttemptTarget) (FilterContext, error) {
	bc := NewFilterContextBuilder(s.baseCtx.ConfigGeneration(), s.baseCtx.AttemptID())
	bc = bc.SetEnvironment(s.baseCtx.Environment())
	bc = bc.SetEndpoint(s.baseCtx.Endpoint())
	bc = bc.SetFamily(s.baseCtx.Family())
	bc = bc.SetModelGroup(target.ModelGroup())
	bc = bc.SetActualModel(target.Model())
	bc = bc.SetActualProvider(target.Provider())
	bc = bc.SetExecutionPath(target.ExecutionPath())
	bc = bc.SetStream(s.baseCtx.StreamID())
	bc = bc.SetCommitState(s.baseCtx.CommitState())
	bc = bc.SetHasToolSideEffect(s.baseCtx.HasToolSideEffect())
	bc = bc.SetTerminal(s.baseCtx.Terminal())
	bc = bc.SetStableCorrelation(s.baseCtx.StableCorrelation())
	return bc.Build()
}

// FilterRegistrySnapshot is the generation-bound immutable snapshot of all
// registrations and policy.
type FilterRegistrySnapshot struct {
	generation    string
	registrations []FilterRegistration
	policies      []FilterPolicyLayer
	resolvedIndex map[string]resolvedPolicy
}

// NewFilterRegistrySnapshot validates and snapshots registrations and
// policies into a generation-bound snapshot. Registration identities are
// verified against live filter IDs at construction time.
func NewFilterRegistrySnapshot(
	generation string,
	registrations []FilterRegistration,
	policies []FilterPolicyLayer,
) (FilterRegistrySnapshot, error) {
	if generation == "" {
		return FilterRegistrySnapshot{}, errors.New("streamgate: filter registry snapshot generation is required")
	}

	seen := make(map[string]StableToken, len(registrations))
	copiedRegs := make([]FilterRegistration, len(registrations))
	knownIDs := make(map[string]struct{}, len(registrations))
	for i, reg := range registrations {
		fID := reg.FilterID()
		if fID == "" {
			return FilterRegistrySnapshot{}, fmt.Errorf("streamgate: registry snapshot registration %d filter id is required", i)
		}
		if _, dup := knownIDs[fID]; dup {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot: %w (%s)", ErrDuplicateRegistration, fID,
			)
		}
		knownIDs[fID] = struct{}{}
		seen[fID] = reg.RegistrationID()
		// Verify registration identity: the captured filter ID must match
		// the live filter ID at snapshot construction time.
		if err := checkRegistrationIdentity(reg); err != nil {
			return FilterRegistrySnapshot{}, err
		}
		copiedRegs[i] = reg
	}

	copiedPolys := make([]FilterPolicyLayer, len(policies))
	type pk struct {
		fid string
		st  PolicySelectorType
		key string
	}
	seenPolicy := make(map[pk]struct{}, len(policies))
	for i, p := range policies {
		if _, ok := knownIDs[p.FilterID()]; !ok {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot policy %d references unknown filter id: %s",
				i, p.FilterID(),
			)
		}
		if err := p.SelectorType().Validate(); err != nil {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot policy %d selector type: %w", i, err,
			)
		}
		if err := p.Enforcement().Validate(); err != nil {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot policy %d enforcement: %w", i, err,
			)
		}
		if p.Timeout() <= 0 {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot policy %d timeout must be positive", i,
			)
		}
		if p.Priority() < 0 {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot policy %d priority must be non-negative", i,
			)
		}
		canonicalKey := p.SelectorKeyWithDefault()
		if p.SelectorType() == PolicySelectorDefault && canonicalKey != "" {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot policy %d: default selector must have empty key", i,
			)
		}
		if p.SelectorType() != PolicySelectorDefault && canonicalKey == "" {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot policy %d: non-default selector must have non-empty key", i,
			)
		}
		pk := pk{fid: p.FilterID(), st: p.SelectorType(), key: canonicalKey}
		if _, dup := seenPolicy[pk]; dup {
			return FilterRegistrySnapshot{}, fmt.Errorf(
				"streamgate: registry snapshot duplicate policy selector %s=%q for filter %s",
				string(pk.st), pk.key, pk.fid,
			)
		}
		seenPolicy[pk] = struct{}{}
		copiedPolys[i] = p
	}

	index := make(map[string]resolvedPolicy, len(registrations))
	for _, reg := range registrations {
		fID := reg.FilterID()
		index[fID] = resolvedPolicy{
			enabled:     reg.Enabled(),
			enforcement: reg.Enforcement(),
			timeout:     reg.Timeout(),
			priority:    reg.Priority(),
		}
	}

	return FilterRegistrySnapshot{
		generation:    generation,
		registrations: copiedRegs,
		policies:      copiedPolys,
		resolvedIndex: index,
	}, nil
}

// specificityOrder returns the specificity rank for each selector type.
var specificityOrder = map[PolicySelectorType]int{
	PolicySelectorDefault:     0,
	PolicySelectorEnvironment: 1,
	PolicySelectorModelGroup:  2,
	PolicySelectorModel:       3,
	PolicySelectorProvider:    4,
}

// Generation returns the config generation this snapshot is bound to.
func (s FilterRegistrySnapshot) Generation() string { return s.generation }

// Registrations returns a defensive copy of the registered filter set.
func (s FilterRegistrySnapshot) Registrations() []FilterRegistration {
	if s.registrations == nil {
		return nil
	}
	cp := make([]FilterRegistration, len(s.registrations))
	copy(cp, s.registrations)
	return cp
}

// resolvePolicyForContext resolves the effective policy for a registration
// given the execution context. Only policies belonging to this filter's
// registration id are considered.
func (s FilterRegistrySnapshot) resolvePolicyForContext(
	reg FilterRegistration,
	ctx FilterContext,
) resolvedPolicy {
	return s.effectiveEnabledForContext(reg, ctx)
}

// selectorMatchesContext returns true when the policy selector matches the
// execution context.
func selectorMatchesContext(p FilterPolicyLayer, ctx FilterContext) bool {
	switch p.SelectorType() {
	case PolicySelectorDefault:
		return true
	case PolicySelectorEnvironment:
		return ctx.Environment() == p.SelectorKey()
	case PolicySelectorModelGroup:
		return ctx.ModelGroup() == p.SelectorKey()
	case PolicySelectorModel:
		return ctx.ActualModel() == p.SelectorKey()
	case PolicySelectorProvider:
		return ctx.ActualProvider() == p.SelectorKey()
	}
	return false
}

// BeginRequest creates a RequestFilterSnapshot from this generation-bound
// snapshot. Registration identities are validated at the begin boundary.
func (s FilterRegistrySnapshot) BeginRequest(
	ctx RequestFilterContext,
) (RequestFilterSnapshot, error) {
	return NewRequestFilterSnapshot(s, ctx)
}

// validateRegistrationIdentity verifies that all registrations' captured
// filter IDs match their live filter IDs. The stage label provides context
// for the returned error.
func (s *FilterRegistrySnapshot) validateRegistrationIdentity(stage string) error {
	for _, reg := range s.registrations {
		if err := checkRegistrationIdentity(reg); err != nil {
			return fmt.Errorf("streamgate: %s %w", stage, err)
		}
	}
	return nil
}

// resolveTargetWithCtx resolves all registrations against the given context
// and returns the enabled+applicable resolved filters and the union of their
// required capability ids. The resolver validates registration identity,
// applies disabled early exclusion, evaluates target-aware Applies, and
// validates HoldRequirement.
func (s *FilterRegistrySnapshot) resolveTargetWithCtx(
	ctx FilterContext,
) ([]ResolvedFilter, []string, error) {
	var resolved []ResolvedFilter
	caps := make(map[string]struct{})

	for _, reg := range s.registrations {
		// 1. Identity check.
		if err := checkRegistrationIdentity(reg); err != nil {
			return nil, nil, err
		}

		// 2. Effective policy.
		eff := s.resolvePolicyForContext(reg, ctx)

		// 3. Disabled early exclusion — Applies/HoldRequirement are not
		//    called for disabled filters.
		if !eff.enabled {
			continue
		}

		// 4. Target-aware Applies.
		if !reg.filter.Applies(ctx) {
			continue
		}

		// 5. Validated HoldRequirement.
		req := reg.filter.HoldRequirement(ctx)
		if err := req.Validate(); err != nil {
			return nil, nil, fmt.Errorf(
				"streamgate: resolve target hold requirement: %w", err,
			)
		}

		resolved = append(resolved, ResolvedFilter{
			reg:         reg,
			baseCtx:     ctx,
			enabled:     eff.enabled,
			enforcement: eff.enforcement,
			timeout:     eff.timeout,
			priority:    eff.priority,
			requirement: req,
		})
		caps[reg.RequiredCapabilityID()] = struct{}{}
	}

	out := make([]string, 0, len(caps))
	for c := range caps {
		out = append(out, c)
	}
	sort.Strings(out)

	return resolved, out, nil
}

// checkRegistrationIdentity compares a registration's captured filter ID
// with its live filter ID and returns ErrRegistrationFilterIDMismatch if
// they differ.
func checkRegistrationIdentity(reg FilterRegistration) error {
	if reg.filter.ID() != reg.RegistrationID().String() {
		return fmt.Errorf(
			"%w (registration=%s, filter=%s)",
			ErrRegistrationFilterIDMismatch,
			reg.RegistrationID().String(), reg.filter.ID(),
		)
	}
	return nil
}

// candidateHasAll returns true when the candidate capability set includes
// every id in the required list.
func candidateHasAll(candidate AttemptTarget, required []string) bool {
	for _, req := range required {
		if !candidate.HasCapability(req) {
			return false
		}
	}
	return true
}

// ErrNoEligibleCandidate is returned when no candidate capability set covers
// every required filter capability for the request.
var ErrNoEligibleCandidate = errors.New("streamgate: no eligible candidate covers all required capabilities")

// ErrRegistrationFilterIDMismatch is returned when a registration's captured
// filter id does not match the filter's own ID.
var ErrRegistrationFilterIDMismatch = errors.New("streamgate: registration filter id mismatch")

// ErrDuplicateRegistration is returned when a registry snapshot contains
// duplicate filter ids.
var ErrDuplicateRegistration = errors.New("streamgate: registry snapshot duplicate filter id")
