package openai

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"
	"unicode/utf8"

	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/config"
	"iop/packages/go/streamgate"
)

// chatRequestHasSchemeMetadata reports whether the request carries a non-empty
// metadata.scheme output contract. Presence, not shape, is enough to decide
// whether the schema_gate filter participates; JSON-schema validation itself is
// the schema-contract Task's scope.
func chatRequestHasSchemeMetadata(metadata json.RawMessage) bool {
	if len(metadata) == 0 {
		return false
	}
	var m map[string]json.RawMessage
	if err := json.Unmarshal(metadata, &m); err != nil {
		return false
	}
	raw, ok := m["scheme"]
	if !ok {
		return false
	}
	trimmed := strings.TrimSpace(string(raw))
	return trimmed != "" && trimmed != "null"
}

// This file translates the request-stable stream_evidence_gate filter policy
// (packages/go/config) into Core FilterRegistrations and FilterPolicyLayers, and
// resolves required output-filter capabilities for provider admission. The Core
// owns selector precedence, per-target Applies re-resolution, and eligibility;
// this file only builds the generation-bound inputs and adapts the config
// enums to the Core enums.

// openAIOutputFilterContext carries the immutable request-start facts used by
// selector resolution and endpoint-specific filter participation.
type openAIOutputFilterContext struct {
	environment string
	endpoint    string
	modelGroup  string
	hasScheme   bool
	requestRef  string
	history     openAIRepeatHistorySnapshot
}

const (
	openAIRepeatHistoryAnchorThreshold = 2
	openAIRepeatHistoryMaxFingerprints = 1024
	openAIRepeatHistoryMaxActions      = 256
)

type openAIRepeatChannel string

const (
	openAIRepeatChannelContent   openAIRepeatChannel = "content"
	openAIRepeatChannelReasoning openAIRepeatChannel = "reasoning"
)

type openAIRepeatHistoryAction struct {
	actionFingerprint streamgate.FixedFingerprint
	resultFingerprint streamgate.FixedFingerprint
}

type openAIRepeatHistoryCandidate struct {
	count           int
	normalizedRunes int
}

// openAIRepeatHistorySnapshot is the immutable, raw-free semantic view passed
// from an endpoint decoder to the request-local repeat filter. It intentionally
// has no session, caller, TTL, raw text, tool arguments, or tool result fields.
type openAIRepeatHistorySnapshot struct {
	assistantOccurrences map[openAIRepeatChannel]map[streamgate.FixedFingerprint]int
	userOccurrences      map[streamgate.FixedFingerprint]int
	assistantCandidates  map[openAIRepeatChannel]map[streamgate.FixedFingerprint]openAIRepeatHistoryCandidate
	repeatedActions      map[streamgate.FixedFingerprint]int
	hasCompletedProgress bool
	hasTerminalProgress  bool
}

type openAIRepeatHistoryBuilder struct {
	assistantOccurrences map[openAIRepeatChannel]map[streamgate.FixedFingerprint]int
	userOccurrences      map[streamgate.FixedFingerprint]int
	normalizedTextRunes  map[streamgate.FixedFingerprint]int
	completedActions     []openAIRepeatHistoryAction
	hasTerminalProgress  bool
}

func newOpenAIRepeatHistoryBuilder() *openAIRepeatHistoryBuilder {
	return &openAIRepeatHistoryBuilder{
		assistantOccurrences: map[openAIRepeatChannel]map[streamgate.FixedFingerprint]int{
			openAIRepeatChannelContent:   {},
			openAIRepeatChannelReasoning: {},
		},
		userOccurrences:     make(map[streamgate.FixedFingerprint]int),
		normalizedTextRunes: make(map[streamgate.FixedFingerprint]int),
	}
}

func normalizeOpenAIRepeatText(value string) string {
	return strings.Join(strings.Fields(value), " ")
}

func openAIRepeatTextFingerprint(value string) (streamgate.FixedFingerprint, bool) {
	normalized := normalizeOpenAIRepeatText(value)
	if normalized == "" {
		return streamgate.FixedFingerprint{}, false
	}
	return streamgate.FixedFingerprint(sha256.Sum256([]byte(normalized))), true
}

func (b *openAIRepeatHistoryBuilder) admitTextFingerprint(fingerprint streamgate.FixedFingerprint, normalizedRunes int) bool {
	if _, exists := b.normalizedTextRunes[fingerprint]; exists {
		return true
	}
	if len(b.normalizedTextRunes) >= openAIRepeatHistoryMaxFingerprints {
		return false
	}
	b.normalizedTextRunes[fingerprint] = normalizedRunes
	return true
}

func openAIRepeatActionFingerprint(name string, arguments json.RawMessage) (streamgate.FixedFingerprint, bool) {
	name = normalizeOpenAIRepeatText(name)
	if name == "" {
		return streamgate.FixedFingerprint{}, false
	}
	var canonical any
	if len(arguments) > 0 && json.Unmarshal(arguments, &canonical) == nil {
		if encoded, err := json.Marshal(canonical); err == nil {
			arguments = encoded
		}
	}
	normalizedArgs := normalizeOpenAIRepeatText(string(arguments))
	return streamgate.FixedFingerprint(sha256.Sum256([]byte(name + "\x00" + normalizedArgs))), true
}

func (b *openAIRepeatHistoryBuilder) recordText(role string, channel openAIRepeatChannel, value string) {
	role = strings.ToLower(strings.TrimSpace(role))
	if role != "assistant" && role != "user" {
		return
	}
	normalized := normalizeOpenAIRepeatText(value)
	if normalized == "" {
		return
	}
	fingerprint := streamgate.FixedFingerprint(sha256.Sum256([]byte(normalized)))
	if !b.admitTextFingerprint(fingerprint, utf8.RuneCountInString(normalized)) {
		return
	}
	switch role {
	case "assistant":
		if b.assistantOccurrences[channel] == nil {
			b.assistantOccurrences[channel] = make(map[streamgate.FixedFingerprint]int)
		}
		b.assistantOccurrences[channel][fingerprint]++
	case "user":
		b.userOccurrences[fingerprint]++
	}
}

func (b *openAIRepeatHistoryBuilder) recordCompletedAction(action, result streamgate.FixedFingerprint) {
	if action == (streamgate.FixedFingerprint{}) || result == (streamgate.FixedFingerprint{}) {
		return
	}
	if len(b.completedActions) >= openAIRepeatHistoryMaxActions {
		return
	}
	b.completedActions = append(b.completedActions, openAIRepeatHistoryAction{
		actionFingerprint: action,
		resultFingerprint: result,
	})
}

func (b *openAIRepeatHistoryBuilder) snapshot() openAIRepeatHistorySnapshot {
	snapshot := openAIRepeatHistorySnapshot{
		assistantOccurrences: map[openAIRepeatChannel]map[streamgate.FixedFingerprint]int{
			openAIRepeatChannelContent:   {},
			openAIRepeatChannelReasoning: {},
		},
		userOccurrences: map[streamgate.FixedFingerprint]int{},
		assistantCandidates: map[openAIRepeatChannel]map[streamgate.FixedFingerprint]openAIRepeatHistoryCandidate{
			openAIRepeatChannelContent:   {},
			openAIRepeatChannelReasoning: {},
		},
		repeatedActions:     make(map[streamgate.FixedFingerprint]int),
		hasTerminalProgress: b.hasTerminalProgress,
	}
	for fingerprint, count := range b.userOccurrences {
		snapshot.userOccurrences[fingerprint] = count
	}
	totalAssistant := make(map[streamgate.FixedFingerprint]int)
	for channel, occurrences := range b.assistantOccurrences {
		for fingerprint, count := range occurrences {
			snapshot.assistantOccurrences[channel][fingerprint] = count
			totalAssistant[fingerprint] += count
		}
	}
	for channel, occurrences := range snapshot.assistantOccurrences {
		for fingerprint := range occurrences {
			if snapshot.userOccurrences[fingerprint] == 0 &&
				totalAssistant[fingerprint] >= openAIRepeatHistoryAnchorThreshold {
				snapshot.assistantCandidates[channel][fingerprint] = openAIRepeatHistoryCandidate{
					count:           totalAssistant[fingerprint],
					normalizedRunes: b.normalizedTextRunes[fingerprint],
				}
			}
		}
	}
	if len(b.completedActions) >= 2 {
		previous := b.completedActions[len(b.completedActions)-2]
		current := b.completedActions[len(b.completedActions)-1]
		snapshot.hasCompletedProgress =
			previous.resultFingerprint != current.resultFingerprint
		if previous.actionFingerprint == current.actionFingerprint &&
			previous.resultFingerprint == current.resultFingerprint {
			snapshot.repeatedActions[current.actionFingerprint] = 1
		}
	}
	return snapshot
}

func (s openAIRepeatHistorySnapshot) assistantCandidate(channel openAIRepeatChannel, fingerprint streamgate.FixedFingerprint) (int, bool) {
	candidate, ok := s.assistantCandidates[channel][fingerprint]
	return candidate.count, ok
}

func (s openAIRepeatHistorySnapshot) assistantCandidateMetadata(channel openAIRepeatChannel, fingerprint streamgate.FixedFingerprint) (openAIRepeatHistoryCandidate, bool) {
	candidate, ok := s.assistantCandidates[channel][fingerprint]
	return candidate, ok
}

func (s openAIRepeatHistorySnapshot) repeatedAction(fingerprint streamgate.FixedFingerprint) (int, bool) {
	count, ok := s.repeatedActions[fingerprint]
	return count, ok
}

func (s openAIRepeatHistorySnapshot) completedProgress() bool {
	return s.hasCompletedProgress || s.hasTerminalProgress
}

// streamgateFilterEnforcement adapts a config enforcement string to the Core
// enforcement enum, defaulting to blocking for empty/unknown input (validation
// rejects unknown values before this point).
func streamgateFilterEnforcement(s string) streamgate.FilterEnforcement {
	if s == config.StreamGateFilterEnforcementObserveOnly {
		return streamgate.FilterEnforcementObserveOnly
	}
	return streamgate.FilterEnforcementBlocking
}

// streamgateSelectorType adapts a config selector type to the Core selector
// type. It returns false when the type is unknown.
func streamgateSelectorType(s string) (streamgate.PolicySelectorType, bool) {
	switch s {
	case config.StreamGateFilterSelectorEnvironment:
		return streamgate.PolicySelectorEnvironment, true
	case config.StreamGateFilterSelectorModelGroup:
		return streamgate.PolicySelectorModelGroup, true
	case config.StreamGateFilterSelectorModel:
		return streamgate.PolicySelectorModel, true
	case config.StreamGateFilterSelectorProvider:
		return streamgate.PolicySelectorProvider, true
	default:
		return "", false
	}
}

// openAIOutputFilterRegistrations builds the Core registrations and policy
// layers for the configured semantic output filters. A schema_gate filter is
// only registered when the caller requested a schema contract; a request with
// no scheme neither registers nor requires it. The returned slices are the
// request-stable inputs to a generation-bound FilterRegistrySnapshot.
func openAIOutputFilterRegistrations(gateCfg config.StreamEvidenceGateConf, fctx openAIOutputFilterContext) ([]streamgate.FilterRegistration, []streamgate.FilterPolicyLayer, error) {
	// The response runtime and its private liveness registration are always
	// present on supported OpenAI paths. This gate controls configured semantic
	// filters and their capability admission only.
	if !gateCfg.Enabled {
		return nil, nil, nil
	}
	var (
		regs     []streamgate.FilterRegistration
		policies []streamgate.FilterPolicyLayer
	)
	for _, fc := range gateCfg.Filters {
		if fc.Filter == config.StreamGateFilterSchemaGate && !fctx.hasScheme {
			continue
		}

		filter, err := newOpenAIOutputFilter(
			openAIOutputFilterKind(fc.Filter),
			fc.EffectiveHoldEvidenceRunes(),
			fc.Priority,
			fctx.requestRef,
			fctx.history,
		)
		if err != nil {
			return nil, nil, err
		}
		filters := []*openAIOutputFilter{filter}
		if fc.Filter == config.StreamGateFilterRepeatGuard {
			actionFilter, actionErr := newOpenAIOutputFilter(
				openAIOutputFilterRepeatActionGuard,
				fc.EffectiveHoldEvidenceRunes(),
				fc.Priority,
				fctx.requestRef,
				fctx.history,
			)
			if actionErr != nil {
				return nil, nil, actionErr
			}
			filters = append(filters, actionFilter)
		}
		timeout := time.Duration(fc.EffectiveTimeoutMS()) * time.Millisecond
		for _, registeredFilter := range filters {
			reg, err := streamgate.NewFilterRegistration(
				registeredFilter, fc.EffectiveCapability(), fc.EffectiveEnabled(),
				streamgateFilterEnforcement(fc.EffectiveEnforcement()), timeout, fc.Priority,
			)
			if err != nil {
				return nil, nil, err
			}
			regs = append(regs, reg)

			for i, sel := range fc.Selectors {
				selType, ok := streamgateSelectorType(sel.Type)
				if !ok {
					return nil, nil, fmt.Errorf("openai output filter %q selector %d unknown type %q", fc.Filter, i, sel.Type)
				}
				enabled := fc.EffectiveEnabled()
				if sel.Enabled != nil {
					enabled = *sel.Enabled
				}
				enforcement := fc.EffectiveEnforcement()
				if sel.Enforcement != "" {
					enforcement = sel.Enforcement
				}
				layer, err := streamgate.NewFilterPolicyLayer(
					registeredFilter.ID(), selType, sel.Key, enabled,
					streamgateFilterEnforcement(enforcement), timeout, fc.Priority,
				)
				if err != nil {
					return nil, nil, err
				}
				policies = append(policies, layer)
			}
		}
	}
	return regs, policies, nil
}

// openAIOutputFilterRequestContext builds the caller-neutral RequestFilterContext
// used to resolve required capabilities and eligibility. It intentionally
// carries no caller/agent product name: the same payload resolves identically
// for raw HTTP, OpenAI SDK, or any other caller.
func openAIOutputFilterRequestContext(fctx openAIOutputFilterContext) (streamgate.RequestFilterContext, error) {
	return streamgate.NewRequestFilterContext(
		streamGateConfigGeneration,
		"admission",
		fctx.environment,
		fctx.endpoint,
		openAIRebuildFamily,
		"",
		streamgate.CommitStateTransportUncommitted,
		false,
		false,
		"",
	)
}

// blockingCapabilitiesForTarget resolves effective policy at the actual target
// and excludes observe-only filters from admission.
func blockingCapabilitiesForTarget(reqSnap streamgate.RequestFilterSnapshot, target streamgate.AttemptTarget) ([]string, error) {
	resolved, err := reqSnap.ResolveAttempt(target)
	if err != nil {
		return nil, err
	}
	set := make(map[string]struct{}, len(resolved))
	for _, filter := range resolved {
		if filter.Enforcement() != streamgate.FilterEnforcementBlocking {
			continue
		}
		set[filter.Registration().RequiredCapabilityID()] = struct{}{}
	}
	required := make([]string, 0, len(set))
	for capability := range set {
		required = append(required, capability)
	}
	sort.Strings(required)
	return required, nil
}

func candidateHasCapabilities(target streamgate.AttemptTarget, required []string) bool {
	for _, capability := range required {
		if !target.HasCapability(capability) {
			return false
		}
	}
	return true
}

// openAIStreamGateRequiredCapabilities returns the union of output-filter
// capabilities that the given candidate target must advertise. Only
// enabled+applicable filters at the target contribute.
func openAIStreamGateRequiredCapabilities(gateCfg config.StreamEvidenceGateConf, fctx openAIOutputFilterContext, target streamgate.AttemptTarget) ([]string, error) {
	regs, policies, err := openAIOutputFilterRegistrations(gateCfg, fctx)
	if err != nil {
		return nil, err
	}
	snap, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regs, policies)
	if err != nil {
		return nil, err
	}
	reqCtx, err := openAIOutputFilterRequestContext(fctx)
	if err != nil {
		return nil, err
	}
	reqSnap, err := snap.BeginRequest(reqCtx)
	if err != nil {
		return nil, err
	}
	return blockingCapabilitiesForTarget(reqSnap, target)
}

// errStreamGateRequiredCapabilityUnsupported is the admission-time error a
// handler maps to an OpenAI-compatible invalid_request_error (400) when a
// required output-filter capability is unsupported by every candidate provider,
// before any provider dispatch or recovery budget is consumed.
var errStreamGateRequiredCapabilityUnsupported = fmt.Errorf("stream gate: required output filter capability unsupported by all candidates")

// openAIStreamGateAdmitCandidates returns the subset of candidate targets whose
// capability set covers every required output-filter capability resolved at each
// candidate's own target context. It returns
// errStreamGateRequiredCapabilityUnsupported when no candidate is eligible, so
// the caller rejects the request with a pre-dispatch 400 instead of leaving a
// required filter silently unenforced.
func openAIStreamGateAdmitCandidates(gateCfg config.StreamEvidenceGateConf, fctx openAIOutputFilterContext, candidates []streamgate.AttemptTarget) ([]streamgate.AttemptTarget, error) {
	regs, policies, err := openAIOutputFilterRegistrations(gateCfg, fctx)
	if err != nil {
		return nil, err
	}
	// No configured output filters => no required capability => every candidate
	// is admissible; preserve the existing provider-pool admission unchanged.
	if len(regs) == 0 {
		return candidates, nil
	}
	snap, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regs, policies)
	if err != nil {
		return nil, err
	}
	reqCtx, err := openAIOutputFilterRequestContext(fctx)
	if err != nil {
		return nil, err
	}
	reqSnap, err := snap.BeginRequest(reqCtx)
	if err != nil {
		return nil, err
	}
	eligible := make([]streamgate.AttemptTarget, 0, len(candidates))
	for _, candidate := range candidates {
		required, err := blockingCapabilitiesForTarget(reqSnap, candidate)
		if err != nil {
			return nil, err
		}
		if candidateHasCapabilities(candidate, required) {
			eligible = append(eligible, candidate)
		}
	}
	if len(eligible) == 0 {
		return nil, errStreamGateRequiredCapabilityUnsupported
	}
	return eligible, nil
}

// openAIStreamGateCandidatePredicate turns one immutable request policy into a
// provider-pool admission predicate. Service invokes it before the first
// dispatch and on every recovery/queue re-resolution, so provider-specific
// selectors are evaluated against the actual selected target rather than a
// caller-supplied model or agent identity.
func openAIStreamGateCandidatePredicate(gateCfg config.StreamEvidenceGateConf, fctx openAIOutputFilterContext) (edgeservice.ProviderPoolCandidatePredicate, error) {
	regs, _, err := openAIOutputFilterRegistrations(gateCfg, fctx)
	if err != nil {
		return nil, err
	}
	if len(regs) == 0 {
		return nil, nil
	}
	return func(candidate edgeservice.ProviderPoolCandidate) bool {
		target, err := streamgate.NewAttemptTarget(
			fctx.modelGroup,
			candidate.ActualModel,
			candidate.ProviderID,
			candidate.ExecutionPath,
			candidate.LifecycleCapabilities,
		)
		if err != nil {
			return false
		}
		eligible, err := openAIStreamGateAdmitCandidates(gateCfg, fctx, []streamgate.AttemptTarget{target})
		return err == nil && len(eligible) == 1
	}, nil
}
