package run

import (
	"bytes"
	"encoding/json"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

// Values is one typed object at a Plan, node, branch, loop, or Run boundary.
type Values map[string]any

// NormalizeContract validates a dynamic value against executable artifact
// constraints, applies defaults, rejects undeclared fields, and returns JSON-
// deterministic runtime representations (notably json.Number and UTC datetimes).
func NormalizeContract(path string, contract plan.ArtifactContract, value Values) (Values, error) {
	if path == "" {
		path = "value"
	}
	if value == nil {
		value = Values{}
	}
	declared := make(map[string]plan.ArtifactSchema, len(contract))
	for _, field := range contract {
		declared[field.Name] = field.Schema
	}
	for name := range value {
		if _, ok := declared[name]; !ok {
			return nil, validationErrorf(validationCodeOutputUndeclaredField, path, name, nil,
				"%s contains undeclared field %q", path, name)
		}
	}
	result := make(Values, len(contract))
	for _, field := range contract {
		raw, present := value[field.Name]
		if !present {
			if field.Schema.Default != nil {
				decoded, err := decodeLiteral(*field.Schema.Default)
				if err != nil {
					return nil, validationErrorf(validationCodeOutputDefaultInvalid, path+"."+field.Name, field.Name, nil,
						"%s.%s default: %w", path, field.Name, err)
				}
				raw, present = decoded, true
			} else if field.Schema.Required {
				return nil, validationErrorf(validationCodeOutputMissingRequired, path, field.Name, nil,
					"%s is missing required field %q", path, field.Name)
			}
		}
		if !present {
			continue
		}
		normalized, err := normalizeSchemaValue(path+"."+field.Name, field.Schema, raw)
		if err != nil {
			return nil, err
		}
		result[field.Name] = normalized
	}
	return result, nil
}

// ValidateContract is the non-transforming convenience form.
func ValidateContract(path string, contract plan.ArtifactContract, value Values) error {
	_, err := NormalizeContract(path, contract, value)
	return err
}

func normalizeSchemaValue(path string, schema plan.ArtifactSchema, value any) (any, error) {
	field := ""
	if last := strings.LastIndex(path, "."); last >= 0 && last+1 < len(path) {
		field = path[last+1:]
	}
	switch schema.Type {
	case plan.TypeChannel, plan.TypeString, plan.TypeMember, plan.TypeAgent:
		text, ok := value.(string)
		if !ok {
			return nil, validationErrorf(validationCodeOutputTypeInvalid, path, field, nil,
				"%s must be a %s string", path, schema.Type)
		}
		return text, nil
	case plan.TypeResponder:
		return normalizeResponderSchemaValue(path, value)
	case plan.TypeEnum:
		text, ok := value.(string)
		if !ok {
			return nil, validationErrorf(validationCodeOutputEnumInvalid, path, field, nil,
				"%s must be an enum string", path)
		}
		allowed := false
		for _, candidate := range schema.Values {
			if candidate == text {
				allowed = true
				break
			}
		}
		if !allowed {
			return nil, validationErrorf(validationCodeOutputEnumInvalid, path, field, nil,
				"%s has disallowed enum value %q", path, text)
		}
		return text, nil
	case plan.TypeNumber:
		number, err := normalizeNumber(value)
		if err != nil {
			return nil, validationErrorf(validationCodeOutputNumberInvalid, path, field, nil,
				"%s must be a finite JSON number: %w", path, err)
		}
		return number, nil
	case plan.TypeBoolean:
		boolean, ok := value.(bool)
		if !ok {
			return nil, validationErrorf(validationCodeOutputBooleanInvalid, path, field, nil,
				"%s must be a boolean", path)
		}
		return boolean, nil
	case plan.TypeDuration:
		number, err := normalizeNumber(value)
		if err != nil {
			return nil, validationErrorf(validationCodeOutputDurationInvalid, path, field, nil,
				"%s must be positive int64 nanoseconds", path)
		}
		nanos, err := strconv.ParseInt(number.String(), 10, 64)
		if err != nil || nanos <= 0 {
			return nil, validationErrorf(validationCodeOutputDurationInvalid, path, field, nil,
				"%s must be positive int64 nanoseconds", path)
		}
		return json.Number(strconv.FormatInt(nanos, 10)), nil
	case plan.TypeDatetime:
		text, ok := value.(string)
		if !ok {
			return nil, validationErrorf(validationCodeOutputDatetimeInvalid, path, field, nil,
				"%s must be a canonical UTC RFC3339Nano string", path)
		}
		instant, err := time.Parse(time.RFC3339, text)
		if err != nil || text != instant.UTC().Format(time.RFC3339Nano) {
			return nil, validationErrorf(validationCodeOutputDatetimeInvalid, path, field, nil,
				"%s must be a canonical UTC RFC3339Nano string", path)
		}
		return text, nil
	case plan.TypeScope:
		scope, err := domain.NormalizeMessageScopeValue(value)
		if err != nil {
			return nil, validationErrorf(validationCodeOutputScopeInvalid, path, field, nil,
				"%s must be a canonical message scope: %w", path, err)
		}
		return map[string]any{
			"kind":    string(scope.Kind),
			"scopeId": scope.ScopeID,
		}, nil
	case plan.TypeObject:
		var object map[string]any
		switch typed := value.(type) {
		case map[string]any:
			object = typed
		case Values:
			object = map[string]any(typed)
		default:
			return nil, validationErrorf(validationCodeOutputObjectInvalid, path, field, nil,
				"%s must be an object", path)
		}
		return normalizeJSONObject(path, object)
	case plan.TypeArray:
		array, ok := value.([]any)
		if !ok || schema.Items == nil {
			return nil, validationErrorf(validationCodeOutputArrayInvalid, path, field, nil,
				"%s must be an array", path)
		}
		result := make([]any, len(array))
		for index, item := range array {
			normalized, err := normalizeSchemaValue(fmt.Sprintf("%s[%d]", path, index), *schema.Items, item)
			if err != nil {
				return nil, err
			}
			result[index] = normalized
		}
		return result, nil
	case plan.TypeWorkItems:
		return normalizeWorkItems(path, value)
	default:
		return nil, validationErrorf(validationCodeOutputUnsupportedType, path, field, nil,
			"%s has unsupported type %q", path, schema.Type)
	}
}

// normalizeResponderSchemaValue validates the fixed-agent/selectOne union. The
// allowlist was enforced when the WorkItem was authorized; this boundary keeps
// only the structural limits while normalizing the frozen value. The
// optional `strategy` sibling (ordered|random) is preserved losslessly.
func normalizeResponderSchemaValue(path string, value any) (any, error) {
	if text, ok := value.(string); ok {
		text = strings.TrimSpace(text)
		if text == "" {
			return nil, validationErrorf(validationCodeOutputResponderInvalid, path, "", nil,
				"%s must be a non-blank agent", path)
		}
		return text, nil
	}
	object, ok := value.(map[string]any)
	if !ok {
		return nil, validationErrorf(validationCodeOutputResponderInvalid, path, "", nil,
			"%s must be a fixed agent string or a selectOne candidate pool", path)
	}
	raw, present := object["selectOne"]
	if !present {
		return nil, validationErrorf(validationCodeOutputResponderInvalid, path, "", nil,
			"%s must be a fixed agent string or a selectOne candidate pool", path)
	}
	// The optional `strategy` sibling accepts the same closed values as
	// sendMessage.responder.selectOne (ordered default, random). Every other
	// sibling is rejected so the frozen value cannot smuggle routing fields.
	strategy := plan.SelectOneStrategy("")
	for key, rawValue := range object {
		if key == "selectOne" {
			continue
		}
		if key == "strategy" {
			strategyValue, strategyOK := rawValue.(string)
			if !strategyOK {
				return nil, validationErrorf(validationCodeOutputSelectOneInvalid, path, "", nil,
					"%s.strategy must be a string", path)
			}
			parsed, err := plan.ParseSelectOneStrategyPresent(strategyValue)
			if err != nil {
				return nil, validationErrorf(validationCodeOutputSelectOneInvalid, path, "", nil,
					"%s.strategy: %v", path, err)
			}
			strategy = parsed
			continue
		}
		return nil, validationErrorf(validationCodeOutputSelectOneInvalid, path, "", nil,
			"%s has unsupported responder field %q", path, key)
	}
	values, ok := raw.([]any)
	if !ok {
		return nil, validationErrorf(validationCodeOutputSelectOneInvalid, path, "", nil,
			"%s.selectOne must be an array of agents", path)
	}
	if len(values) == 0 {
		return nil, validationErrorf(validationCodeOutputSelectOneInvalid, path, "", nil,
			"%s.selectOne must not be empty", path)
	}
	if len(values) > plan.MaxAllowedResponders {
		return nil, validationErrorf(validationCodeOutputSelectOneInvalid, path, "", nil,
			"%s.selectOne must not contain more than %d candidates", path, plan.MaxAllowedResponders)
	}
	normalized := make([]any, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for index, rawCandidate := range values {
		candidate, candidateOK := rawCandidate.(string)
		candidate = strings.TrimSpace(candidate)
		if !candidateOK || candidate == "" {
			return nil, validationErrorf(validationCodeOutputSelectOneInvalid, path, "", intPtr(index),
				"%s.selectOne[%d] must be a non-blank agent", path, index)
		}
		if _, duplicate := seen[candidate]; duplicate {
			continue
		}
		seen[candidate] = struct{}{}
		normalized = append(normalized, candidate)
	}
	if strategy.Normalized() == plan.SelectOneStrategyRandom && len(normalized) < 2 {
		return nil, validationErrorf(validationCodeOutputSelectOneInvalid, path, "", nil,
			"%s.selectOne strategy %q requires at least two deduplicated candidates", path, strategy)
	}
	if strategy == "" {
		return map[string]any{"selectOne": normalized}, nil
	}
	return map[string]any{"selectOne": normalized, "strategy": string(strategy)}, nil
}

// WorkItem is the closed, compiler-owned callback value consumed by fan_out.
// It is deliberately separate from WorkItemState: this value is immutable and
// safe to share with callback validation, while state adds execution status.
type WorkItem struct {
	Key           string                     `json:"key"`
	DependsOn     []string                   `json:"dependsOn"`
	WorkScope     domain.MessageScopeValue   `json:"workScope"`
	RelatedScopes []domain.MessageScopeValue `json:"relatedScopes"`
	// Responder is the fixed single-agent responder. It is mutually exclusive
	// with Responders: exactly one is set. Responders carries the frozen
	// ordered candidate pool of a WorkItem selectOne responder, in priority
	// order when several candidates are simultaneously available. SelectOneStrategy
	// is the additive selectOne strategy ("" or "ordered" = strict priority,
	// "random" = injectable unbiased randomization).
	Responder         string                 `json:"responder,omitempty"`
	Responders        []string               `json:"responders,omitempty"`
	SelectOneStrategy plan.SelectOneStrategy `json:"selectOneStrategy,omitempty"`
	Instruction       string                 `json:"instruction"`
}

// normalizeWorkItems validates the compiler-owned callback value before it can
// be frozen by fan_out. Keeping this nominal instead of accepting array<object>
// prevents callback data from smuggling routing or authorization fields.
func normalizeWorkItems(path string, value any) ([]any, error) {
	items, err := NormalizeWorkItems(path, value, nil)
	if err != nil {
		return nil, err
	}
	return WorkItemsValue(items), nil
}

// NormalizeWorkItems is the single normalized validator shared by callback
// receipt handling and the pure fan-out reducer. A nil allowedResponders slice
// skips allowlist enforcement (the fan-out path has already consumed an
// authorized callback); a non-nil slice is normalized and enforced exactly.
func NormalizeWorkItems(path string, value any, allowedResponders []string) ([]WorkItem, error) {
	items, ok := value.([]any)
	if !ok {
		return nil, validationErrorf(validationCodeWorkItemsNotArray, path, "", nil,
			"%s must be an array of WorkItems", path)
	}
	if len(items) > plan.MaxWorkItems {
		return nil, validationErrorf(validationCodeWorkItemsTooMany, path, "", nil,
			"%s must not contain more than %d WorkItems", path, plan.MaxWorkItems)
	}
	allowed, err := normalizeAllowedResponders(path+".allowedResponders", allowedResponders)
	if err != nil {
		return nil, err
	}
	result := make([]WorkItem, len(items))
	keys := make(map[string]int, len(items))
	dependencies := make([][]string, len(items))
	dependencyEdges := 0
	relatedScopeReferences := 0
	// The root WorkItems array is one deterministic JSON value. Each closed item
	// contributes nine fixed values, one per dependency, and three per related
	// scope ({kind, scopeId}).
	structuralValues := 1
	for index, raw := range items {
		itemPath := fmt.Sprintf("%s[%d]", path, index)
		item, edges, refs, values, err := normalizeWorkItem(path, itemPath, raw, index, allowed, keys, dependencies)
		if err != nil {
			return nil, err
		}
		dependencyEdges += edges
		if dependencyEdges > plan.MaxWorkItemDependencyEdges {
			return nil, validationErrorf(validationCodeWorkItemsDependencyExceeded, path, "", nil,
				"%s must not contain more than %d dependency edges",
				path,
				plan.MaxWorkItemDependencyEdges,
			)
		}
		relatedScopeReferences += refs
		if relatedScopeReferences > plan.MaxWorkItemRelatedScopeReferences {
			return nil, validationErrorf(validationCodeWorkItemsRelatedScopeExceeded, path, "", nil,
				"%s must not contain more than %d related scope references",
				path,
				plan.MaxWorkItemRelatedScopeReferences,
			)
		}
		structuralValues += values
		if structuralValues > plan.MaxWorkItemsJSONValues {
			return nil, validationErrorf(validationCodeWorkItemsJSONValuesExceeded, path, "", nil,
				"%s must not contain more than %d canonical JSON values",
				path,
				plan.MaxWorkItemsJSONValues,
			)
		}
		// normalizeWorkItem already resolved the responder/instruction/strategy
		// and returned the complete closed WorkItem (the #t2587 normalized form).
		result[index] = item
	}
	if err := validateWorkItemGraphAndSize(path, result, dependencies, keys); err != nil {
		return nil, err
	}
	return result, nil
}

// normalizeWorkItem validates and normalizes one WorkItems array element,
// registering its key and dependencies for the caller's DAG and budget checks.
func normalizeWorkItem(
	path string,
	itemPath string,
	raw any,
	index int,
	allowed map[string]struct{},
	keys map[string]int,
	dependencies [][]string,
) (WorkItem, int, int, int, error) {
	object, ok := workItemObject(raw)
	if !ok {
		return WorkItem{}, 0, 0, 0, validationErrorf(validationCodeWorkItemsItemNotObject, itemPath, "", intPtr(index),
			"%s must be an object", itemPath)
	}
	if err := requireWorkItemFields(itemPath, object); err != nil {
		return WorkItem{}, 0, 0, 0, err
	}
	key, err := normalizedWorkItemString(itemPath+"."+plan.WorkItemFieldKey, object[plan.WorkItemFieldKey], false)
	if err != nil {
		return WorkItem{}, 0, 0, 0, err
	}
	if len([]byte(key)) > plan.MaxWorkItemKeyBytes {
		return WorkItem{}, 0, 0, 0, validationErrorf(validationCodeWorkItemsKeyTooLong, itemPath+"."+plan.WorkItemFieldKey, plan.WorkItemFieldKey, intPtr(index),
			"%s.%s must not exceed %d bytes", itemPath, plan.WorkItemFieldKey, plan.MaxWorkItemKeyBytes)
	}
	if err := validateNormalizedWorkItemKeyAt(itemPath+"."+plan.WorkItemFieldKey, key, index); err != nil {
		return WorkItem{}, 0, 0, 0, err
	}
	if previous, duplicate := keys[key]; duplicate {
		return WorkItem{}, 0, 0, 0, validationErrorf(validationCodeWorkItemsKeyDuplicate, itemPath+"."+plan.WorkItemFieldKey, plan.WorkItemFieldKey, intPtr(index),
			"%s.%s duplicates WorkItem %d key %q", itemPath, plan.WorkItemFieldKey, previous, key)
	}
	keys[key] = index
	rawDependsOn, present := object[plan.WorkItemFieldDependsOn]
	if !present {
		rawDependsOn = []any{}
	}
	dependsOn, err := normalizedWorkItemStringArray(itemPath+"."+plan.WorkItemFieldDependsOn, rawDependsOn)
	if err != nil {
		return WorkItem{}, 0, 0, 0, err
	}
	dependencies[index] = dependsOn
	workScope, err := domain.NormalizeMessageScopeValue(object[plan.WorkItemFieldWorkScope])
	if err != nil {
		return WorkItem{}, 0, 0, 0, validationErrorf(validationCodeWorkItemsScopeInvalid, itemPath+"."+plan.WorkItemFieldWorkScope, plan.WorkItemFieldWorkScope, intPtr(index),
			"%s.%s must be a canonical message scope: %w", itemPath, plan.WorkItemFieldWorkScope, err)
	}
	relatedScopes, err := normalizedWorkItemScopes(itemPath+"."+plan.WorkItemFieldRelatedScopes, object[plan.WorkItemFieldRelatedScopes])
	if err != nil {
		return WorkItem{}, 0, 0, 0, err
	}
	responder, responders, strategy, err := normalizedWorkItemResponder(
		itemPath+"."+plan.WorkItemFieldResponder,
		object[plan.WorkItemFieldResponder],
		allowed,
	)
	if err != nil {
		return WorkItem{}, 0, 0, 0, err
	}
	instruction, err := normalizedWorkItemString(itemPath+"."+plan.WorkItemFieldInstruction, object[plan.WorkItemFieldInstruction], true)
	if err != nil {
		return WorkItem{}, 0, 0, 0, err
	}
	if err := validateWorkItemInstructionReferences(
		itemPath+"."+plan.WorkItemFieldInstruction,
		instruction,
		dependsOn,
	); err != nil {
		return WorkItem{}, 0, 0, 0, err
	}
	return WorkItem{
		Key:               key,
		DependsOn:         dependsOn,
		WorkScope:         workScope,
		RelatedScopes:     relatedScopes,
		Responder:         responder,
		Responders:        responders,
		SelectOneStrategy: strategy,
		Instruction:       instruction,
	}, len(dependsOn), len(relatedScopes), 9 + len(dependsOn) + 3*len(relatedScopes), nil
}

func validateWorkItemGraphAndSize(
	path string,
	items []WorkItem,
	dependencies [][]string,
	keys map[string]int,
) error {
	for index, dependsOn := range dependencies {
		key := items[index].Key
		for _, dependency := range dependsOn {
			if dependency == key {
				return validationErrorf(validationCodeWorkItemsSelfDependency, fmt.Sprintf("%s[%d].%s", path, index, plan.WorkItemFieldDependsOn), plan.WorkItemFieldDependsOn, intPtr(index),
					"%s[%d].%s contains its own key %q", path, index, plan.WorkItemFieldDependsOn, key)
			}
			if _, found := keys[dependency]; !found {
				return validationErrorf(validationCodeWorkItemsUnknownDependency, fmt.Sprintf("%s[%d].%s", path, index, plan.WorkItemFieldDependsOn), plan.WorkItemFieldDependsOn, intPtr(index),
					"%s[%d].%s references unknown key %q", path, index, plan.WorkItemFieldDependsOn, dependency)
			}
		}
	}
	if err := validateWorkItemDAG(path, items, dependencies, keys); err != nil {
		return err
	}
	payload, err := json.Marshal(WorkItemsValue(items))
	if err != nil {
		return fmt.Errorf("encode %s: %w", path, err)
	}
	if len(payload) > plan.MaxWorkItemsJSONBytes {
		return validationErrorf(validationCodeWorkItemsJSONBytesExceeded, path, "", nil,
			"%s must not exceed %d canonical JSON bytes",
			path,
			plan.MaxWorkItemsJSONBytes,
		)
	}
	return nil
}

func normalizeAllowedResponders(path string, values []string) (map[string]struct{}, error) {
	if values == nil {
		return nil, nil
	}
	result := make(map[string]struct{}, len(values))
	for index, raw := range values {
		value, err := normalizedWorkItemString(fmt.Sprintf("%s[%d]", path, index), raw, false)
		if err != nil {
			return nil, err
		}
		result[value] = struct{}{}
	}
	if len(result) == 0 {
		return nil, validationErrorf(validationCodeWorkItemsAllowedResponders, path, "", nil,
			"%s must contain at least one responder", path)
	}
	return result, nil
}

func workItemObject(value any) (map[string]any, bool) {
	switch typed := value.(type) {
	case map[string]any:
		return typed, true
	case Values:
		return map[string]any(typed), true
	default:
		return nil, false
	}
}

func requireWorkItemFields(path string, object map[string]any) error {
	allowedFields := []string{
		plan.WorkItemFieldKey,
		plan.WorkItemFieldDependsOn,
		plan.WorkItemFieldWorkScope,
		plan.WorkItemFieldRelatedScopes,
		plan.WorkItemFieldResponder,
		plan.WorkItemFieldInstruction,
	}
	required := []string{
		plan.WorkItemFieldKey,
		plan.WorkItemFieldWorkScope,
		plan.WorkItemFieldRelatedScopes,
		plan.WorkItemFieldResponder,
		plan.WorkItemFieldInstruction,
	}
	allowed := make(map[string]struct{}, len(allowedFields))
	for _, name := range allowedFields {
		allowed[name] = struct{}{}
	}
	for _, name := range required {
		if _, present := object[name]; !present {
			return validationErrorf(validationCodeWorkItemsMissingField, path, name, nil,
				"%s is missing required field %q", path, name)
		}
	}
	for name := range object {
		if _, ok := allowed[name]; !ok {
			return validationErrorf(validationCodeWorkItemsUndeclaredField, path, name, nil,
				"%s contains undeclared field %q", path, name)
		}
	}
	return nil
}

func normalizedWorkItemString(path string, value any, allowWhitespace bool) (string, error) {
	text, ok := value.(string)
	if !ok {
		return "", validationErrorf(validationCodeOutputTypeInvalid, path, "", nil,
			"%s must be a string", path)
	}
	if strings.TrimSpace(text) == "" {
		return "", validationErrorf(validationCodeOutputTypeInvalid, path, "", nil,
			"%s must not be blank", path)
	}
	if !allowWhitespace && strings.TrimSpace(text) != text {
		return "", validationErrorf(validationCodeOutputTypeInvalid, path, "", nil,
			"%s must not have surrounding whitespace", path)
	}
	return text, nil
}

// normalizedWorkItemResponder parses the WorkItem responder union: either a
// fixed single-agent string or a `{selectOne: [...], strategy?}` candidate pool.
// Exactly one form is produced. The optional `strategy` sibling accepts the
// same closed values as sendMessage.responder.selectOne (ordered default,
// random). Every selectOne candidate must belong to the frozen
// allowedResponders allowlist; the pool is deduplicated preserving
// first-occurrence order, must not exceed MaxAllowedResponders, and a random
// pool must have at least two deduplicated candidates.
func normalizedWorkItemResponder(path string, value any, allowed map[string]struct{}) (string, []string, plan.SelectOneStrategy, error) {
	if object, ok := value.(map[string]any); ok {
		raw, present := object["selectOne"]
		if !present {
			return "", nil, "", validationErrorf(validationCodeWorkItemsResponderInvalid, path, "", nil,
				"%s must be a fixed agent string or a selectOne candidate pool", path)
		}
		// The optional `strategy` sibling accepts the same closed values as
		// sendMessage.responder.selectOne (ordered default, random). Every other
		// sibling is rejected so the frozen WorkItem value cannot smuggle routing
		// fields.
		strategy := plan.SelectOneStrategy("")
		for key, rawValue := range object {
			if key == "selectOne" {
				continue
			}
			if key == "strategy" {
				strategyValue, strategyOK := rawValue.(string)
				if !strategyOK {
					return "", nil, "", validationErrorf(validationCodeWorkItemsResponderInvalid, path, "", nil,
						"%s.strategy must be a string", path)
				}
				parsed, err := plan.ParseSelectOneStrategyPresent(strategyValue)
				if err != nil {
					return "", nil, "", validationErrorf(validationCodeWorkItemsResponderInvalid, path, "", nil,
						"%s.strategy: %v", path, err)
				}
				strategy = parsed
				continue
			}
			return "", nil, "", validationErrorf(validationCodeWorkItemsResponderInvalid, path, "", nil,
				"%s has unsupported responder field %q", path, key)
		}
		values, ok := raw.([]any)
		if !ok {
			return "", nil, "", validationErrorf(validationCodeWorkItemsResponderInvalid, path, "", nil,
				"%s.selectOne must be an array of agents", path)
		}
		if len(values) == 0 {
			return "", nil, "", validationErrorf(validationCodeWorkItemsResponderInvalid, path, "", nil,
				"%s.selectOne must not be empty", path)
		}
		if len(values) > plan.MaxAllowedResponders {
			return "", nil, "", validationErrorf(validationCodeWorkItemsResponderInvalid, path, "", nil,
				"%s.selectOne must not contain more than %d candidates",
				path,
				plan.MaxAllowedResponders,
			)
		}
		result := make([]string, 0, len(values))
		seen := make(map[string]struct{}, len(values))
		for index, rawCandidate := range values {
			candidate, err := normalizedWorkItemString(fmt.Sprintf("%s.selectOne[%d]", path, index), rawCandidate, false)
			if err != nil {
				return "", nil, "", err
			}
			if allowed != nil {
				if _, authorized := allowed[candidate]; !authorized {
					return "", nil, "", validationErrorf(validationCodeWorkItemsResponderNotAllowed, fmt.Sprintf("%s.selectOne[%d]", path, index), "", intPtr(index),
						"%s.selectOne[%d] %q is not an allowed responder",
						path,
						index,
						candidate,
					)
				}
			}
			if _, duplicate := seen[candidate]; duplicate {
				continue
			}
			seen[candidate] = struct{}{}
			result = append(result, candidate)
		}
		if strategy.Normalized() == plan.SelectOneStrategyRandom && len(result) < 2 {
			return "", nil, "", validationErrorf(validationCodeWorkItemsResponderInvalid, path, "", nil,
				"%s.selectOne strategy %q requires at least two deduplicated candidates", path, strategy)
		}
		// Preserve presence exactly: absent stays "" (legacy ordered default),
		// explicit "ordered" stays "ordered", "random" stays "random". Never
		// normalize an absent value to the explicit "ordered" string.
		return "", result, strategy, nil
	}
	responder, err := normalizedWorkItemString(path, value, false)
	if err != nil {
		return "", nil, "", err
	}
	if allowed != nil {
		if _, authorized := allowed[responder]; !authorized {
			return "", nil, "", validationErrorf(validationCodeWorkItemsResponderNotAllowed, path, "", nil,
				"%s %q is not an allowed responder", path, responder)
		}
	}
	return responder, nil, "", nil
}

func normalizedWorkItemStringArray(path string, value any) ([]string, error) {
	items, ok := value.([]any)
	if !ok {
		return nil, validationErrorf(validationCodeWorkItemsMissingField, path, "", nil,
			"%s must be an array of strings", path)
	}
	result := make([]string, 0, len(items))
	seen := make(map[string]struct{}, len(items))
	for index, raw := range items {
		item, err := normalizedWorkItemString(fmt.Sprintf("%s[%d]", path, index), raw, false)
		if err != nil {
			return nil, err
		}
		if _, duplicate := seen[item]; duplicate {
			return nil, validationErrorf(validationCodeWorkItemsKeyDuplicate, path, "", intPtr(index),
				"%s contains duplicate key %q", path, item)
		}
		seen[item] = struct{}{}
		result = append(result, item)
	}
	return result, nil
}

func normalizedWorkItemScopes(path string, value any) ([]domain.MessageScopeValue, error) {
	items, ok := value.([]any)
	if !ok {
		return nil, validationErrorf(validationCodeWorkItemsRelatedScopeInvalid, path, "", nil,
			"%s must be an array of scopes", path)
	}
	if len(items) > plan.MaxSendMessageRelatedScopes {
		return nil, validationErrorf(validationCodeWorkItemsRelatedScopeInvalid, path, "", nil,
			"%s must not contain more than %d scopes", path, plan.MaxSendMessageRelatedScopes)
	}
	result := make([]domain.MessageScopeValue, 0, len(items))
	seen := make(map[domain.MessageScopeValue]struct{}, len(items))
	for index, raw := range items {
		scope, err := domain.NormalizeMessageScopeValue(raw)
		if err != nil {
			return nil, validationErrorf(validationCodeWorkItemsRelatedScopeInvalid, fmt.Sprintf("%s[%d]", path, index), "", intPtr(index),
				"%s[%d] must be a canonical message scope: %w", path, index, err)
		}
		if _, duplicate := seen[scope]; duplicate {
			continue
		}
		seen[scope] = struct{}{}
		result = append(result, scope)
	}
	return result, nil
}

func validateWorkItemDAG(path string, items []WorkItem, dependencies [][]string, indexes map[string]int) error {
	const (
		unseen = iota
		visiting
		visited
	)
	marks := make([]int, len(items))
	stack := make([]string, 0, len(items))
	var visit func(int) error
	visit = func(index int) error {
		switch marks[index] {
		case visiting:
			key := items[index].Key
			return validationErrorf(validationCodeWorkItemsDependencyCycle, path, "", nil,
				"%s contains a dependency cycle through %q (%s)", path, key, strings.Join(append(stack, key), " -> "))
		case visited:
			return nil
		}
		marks[index] = visiting
		key := items[index].Key
		stack = append(stack, key)
		for _, dependency := range dependencies[index] {
			if err := visit(indexes[dependency]); err != nil {
				return err
			}
		}
		stack = stack[:len(stack)-1]
		marks[index] = visited
		return nil
	}
	for index := range items {
		if err := visit(index); err != nil {
			return err
		}
	}
	return nil
}

func stringsToAny(values []string) []any {
	result := make([]any, len(values))
	for index, value := range values {
		result[index] = value
	}
	return result
}

func messageScopeObject(scope domain.MessageScopeValue) map[string]any {
	return map[string]any{"kind": string(scope.Kind), "scopeId": scope.ScopeID}
}

func messageScopesToAny(scopes []domain.MessageScopeValue) []any {
	result := make([]any, len(scopes))
	for index, scope := range scopes {
		result[index] = messageScopeObject(scope)
	}
	return result
}

func workItemValue(item WorkItem) map[string]any {
	responder := any(item.Responder)
	if len(item.Responders) > 0 {
		selectOne := map[string]any{"selectOne": stringsToAny(item.Responders)}
		if item.SelectOneStrategy != "" {
			selectOne["strategy"] = string(item.SelectOneStrategy)
		}
		responder = selectOne
	}
	return map[string]any{
		plan.WorkItemFieldKey:           item.Key,
		plan.WorkItemFieldDependsOn:     stringsToAny(item.DependsOn),
		plan.WorkItemFieldWorkScope:     messageScopeObject(item.WorkScope),
		plan.WorkItemFieldRelatedScopes: messageScopesToAny(item.RelatedScopes),
		plan.WorkItemFieldResponder:     responder,
		plan.WorkItemFieldInstruction:   item.Instruction,
	}
}

// WorkItemsValue returns the deterministic JSON-shaped value stored in a typed
// activation output. Callers must use this instead of placing []WorkItem
// directly into Values, because reducer contracts intentionally contain only
// persistence-safe JSON representations.
func WorkItemsValue(items []WorkItem) []any {
	result := make([]any, len(items))
	for index, item := range items {
		copyItem := item
		copyItem.DependsOn = append([]string(nil), item.DependsOn...)
		copyItem.RelatedScopes = append([]domain.MessageScopeValue(nil), item.RelatedScopes...)
		result[index] = workItemValue(copyItem)
	}
	return result
}

func normalizeJSONObject(path string, object map[string]any) (map[string]any, error) {
	result := make(map[string]any, len(object))
	for key, value := range object {
		normalized, err := normalizeJSONValue(path+"."+key, value)
		if err != nil {
			return nil, err
		}
		result[key] = normalized
	}
	return result, nil
}

func normalizeJSONValue(path string, value any) (any, error) {
	switch typed := value.(type) {
	case nil, bool, string:
		return typed, nil
	case json.Number, float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
		return normalizeNumber(typed)
	case map[string]any:
		return normalizeJSONObject(path, typed)
	case Values:
		return normalizeJSONObject(path, map[string]any(typed))
	case []any:
		result := make([]any, len(typed))
		for index, item := range typed {
			normalized, err := normalizeJSONValue(fmt.Sprintf("%s[%d]", path, index), item)
			if err != nil {
				return nil, err
			}
			result[index] = normalized
		}
		return result, nil
	default:
		return nil, validationErrorf(validationCodeOutputJSONValueInvalid, path, "", nil,
			"%s contains non-JSON value %T", path, value)
	}
}

func normalizeNumber(value any) (json.Number, error) {
	var text string
	switch number := value.(type) {
	case json.Number:
		text = number.String()
	case float64:
		if math.IsInf(number, 0) || math.IsNaN(number) {
			return "", fmt.Errorf("non-finite")
		}
		text = strconv.FormatFloat(number, 'g', -1, 64)
	case float32:
		if math.IsInf(float64(number), 0) || math.IsNaN(float64(number)) {
			return "", fmt.Errorf("non-finite")
		}
		text = strconv.FormatFloat(float64(number), 'g', -1, 32)
	case int:
		text = strconv.FormatInt(int64(number), 10)
	case int8:
		text = strconv.FormatInt(int64(number), 10)
	case int16:
		text = strconv.FormatInt(int64(number), 10)
	case int32:
		text = strconv.FormatInt(int64(number), 10)
	case int64:
		text = strconv.FormatInt(number, 10)
	case uint:
		text = strconv.FormatUint(uint64(number), 10)
	case uint8:
		text = strconv.FormatUint(uint64(number), 10)
	case uint16:
		text = strconv.FormatUint(uint64(number), 10)
	case uint32:
		text = strconv.FormatUint(uint64(number), 10)
	case uint64:
		text = strconv.FormatUint(number, 10)
	default:
		return "", fmt.Errorf("got %T", value)
	}
	decoder := json.NewDecoder(strings.NewReader(text))
	decoder.UseNumber()
	var decoded any
	if err := decoder.Decode(&decoded); err != nil {
		return "", err
	}
	number, ok := decoded.(json.Number)
	if !ok {
		return "", fmt.Errorf("not a JSON number")
	}
	return number, nil
}

func decodeLiteral(literal plan.ArtifactLiteral) (any, error) {
	decoder := json.NewDecoder(bytes.NewReader(literal))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return nil, err
	}
	return value, nil
}

func cloneValues(value Values) Values {
	if value == nil {
		return nil
	}
	result := make(Values, len(value))
	for key, item := range value {
		result[key] = cloneValue(item)
	}
	return result
}

func cloneValue(value any) any {
	switch typed := value.(type) {
	case Values:
		return cloneValues(typed)
	case map[string]any:
		result := make(map[string]any, len(typed))
		for key, item := range typed {
			result[key] = cloneValue(item)
		}
		return result
	case []any:
		result := make([]any, len(typed))
		for index, item := range typed {
			result[index] = cloneValue(item)
		}
		return result
	default:
		return typed
	}
}
