package config

import (
	"fmt"
	"reflect"
	"sort"
	"strings"

	"iop/packages/go/singlerequesttemplate"
)

// ExecutionPreset declares one frozen execution shape.
// It carries a fused selector, allowed mode descriptors, per-mode downstream routes,
// and declarative workspace tool alternatives.
type ExecutionPreset struct {
	// ID is the stable, unique preset identifier.
	ID string `mapstructure:"id" yaml:"id"`
	// Selector is the fused selector/planner model binding and options.
	Selector ExecutionModelBinding `mapstructure:"selector" yaml:"selector"`
	// AllowedModes is the set of registered mode descriptors this preset permits.
	AllowedModes []string `mapstructure:"allowed_modes" yaml:"allowed_modes"`
	// Routes maps each allowed mode descriptor to its ordered downstream stages.
	Routes map[string]ExecutionRoute `mapstructure:"routes" yaml:"routes"`
	// WorkspaceTools declares declarative workspace tool binding alternatives.
	WorkspaceTools []ExecutionWorkspaceToolAlternative `mapstructure:"workspace_tools" yaml:"workspace_tools,omitempty"`
	// SingleRequest is the optional operator-owned fixed single-request policy.
	// When set, it pins the preset to an immutable plan→work→review light path
	// with absolute wall-clock, stage-timeout, tool-iteration, and output-byte
	// caps, rejects legacy caller workspace_tools, and enforces high-reasoning
	// binding on the selector and review stage while forbidding it on work.
	SingleRequest *ExecutionSingleRequestPolicy `mapstructure:"single_request" yaml:"single_request,omitempty"`
}

// ExecutionModelBinding declares a canonical model reference and its stage options.
type ExecutionModelBinding struct {
	Model   string         `mapstructure:"model"   yaml:"model"`
	Options map[string]any `mapstructure:"options" yaml:"options,omitempty"`
}

// ExecutionRoute carries the ordered downstream stages for a mode.
type ExecutionRoute struct {
	Stages []ExecutionRouteStage `mapstructure:"stages" yaml:"stages,omitempty"`
}

// ExecutionRouteStage is one ordered downstream stage with role, canonical model, and options.
type ExecutionRouteStage struct {
	Role    string         `mapstructure:"role"    yaml:"role"`
	Model   string         `mapstructure:"model"   yaml:"model"`
	Options map[string]any `mapstructure:"options" yaml:"options,omitempty"`
}

// ExecutionWorkspaceToolAlternative declares one ordered workspace tool binding alternative.
type ExecutionWorkspaceToolAlternative struct {
	Name       string                                 `mapstructure:"name"       yaml:"name"`
	Operations map[string]ExecutionWorkspaceOperation `mapstructure:"operations" yaml:"operations"`
}

// ExecutionWorkspaceOperation declares tool matching, argument mapping, result matching,
// and parent directory creation capability for one workspace operation (prepare, read, write, delete).
type ExecutionWorkspaceOperation struct {
	ToolName       string         `mapstructure:"tool_name"       yaml:"tool_name,omitempty"`
	SchemaMatcher  map[string]any `mapstructure:"schema_matcher"  yaml:"schema_matcher,omitempty"`
	ArgumentMap    map[string]any `mapstructure:"argument_map"    yaml:"argument_map,omitempty"`
	ResultMatcher  map[string]any `mapstructure:"result_matcher"  yaml:"result_matcher,omitempty"`
	CreatesParents bool           `mapstructure:"creates_parents" yaml:"creates_parents,omitempty"`
}

// Clone returns a deep copy of ExecutionPreset.
func (p ExecutionPreset) Clone() ExecutionPreset {
	out := p
	out.Selector = p.Selector.Clone()
	if p.AllowedModes != nil {
		out.AllowedModes = make([]string, len(p.AllowedModes))
		copy(out.AllowedModes, p.AllowedModes)
	}
	if p.Routes != nil {
		out.Routes = make(map[string]ExecutionRoute, len(p.Routes))
		for k, v := range p.Routes {
			out.Routes[k] = v.Clone()
		}
	}
	if p.WorkspaceTools != nil {
		out.WorkspaceTools = make([]ExecutionWorkspaceToolAlternative, len(p.WorkspaceTools))
		for i, wt := range p.WorkspaceTools {
			out.WorkspaceTools[i] = wt.Clone()
		}
	}
	if p.SingleRequest != nil {
		out.SingleRequest = p.SingleRequest.Clone()
	}
	return out
}

// Clone returns a deep copy of ExecutionModelBinding.
func (b ExecutionModelBinding) Clone() ExecutionModelBinding {
	out := b
	out.Options = cloneMapStringAny(b.Options)
	return out
}

// Clone returns a deep copy of ExecutionRoute.
func (r ExecutionRoute) Clone() ExecutionRoute {
	out := r
	if r.Stages != nil {
		out.Stages = make([]ExecutionRouteStage, len(r.Stages))
		for i, st := range r.Stages {
			out.Stages[i] = st.Clone()
		}
	}
	return out
}

// Clone returns a deep copy of ExecutionRouteStage.
func (s ExecutionRouteStage) Clone() ExecutionRouteStage {
	out := s
	out.Options = cloneMapStringAny(s.Options)
	return out
}

// Clone returns a deep copy of ExecutionWorkspaceToolAlternative.
func (wt ExecutionWorkspaceToolAlternative) Clone() ExecutionWorkspaceToolAlternative {
	out := wt
	if wt.Operations != nil {
		out.Operations = make(map[string]ExecutionWorkspaceOperation, len(wt.Operations))
		for k, op := range wt.Operations {
			out.Operations[k] = op.Clone()
		}
	}
	return out
}

// Clone returns a deep copy of ExecutionWorkspaceOperation.
func (op ExecutionWorkspaceOperation) Clone() ExecutionWorkspaceOperation {
	out := op
	out.SchemaMatcher = cloneMapStringAny(op.SchemaMatcher)
	out.ArgumentMap = cloneMapStringAny(op.ArgumentMap)
	out.ResultMatcher = cloneMapStringAny(op.ResultMatcher)
	return out
}

// CloneExecutionPresetCatalog returns a deep copy slice of execution presets.
func CloneExecutionPresetCatalog(presets []ExecutionPreset) []ExecutionPreset {
	if presets == nil {
		return nil
	}
	out := make([]ExecutionPreset, len(presets))
	for i, p := range presets {
		out[i] = p.Clone()
	}
	return out
}

// CanonicalModelReferences returns unique sorted canonical model IDs referenced by selector and allowed route stages.
func (p ExecutionPreset) CanonicalModelReferences() []string {
	seen := make(map[string]struct{})
	var refs []string
	add := func(m string) {
		m = strings.TrimSpace(m)
		if m != "" {
			if _, exists := seen[m]; !exists {
				seen[m] = struct{}{}
				refs = append(refs, m)
			}
		}
	}
	add(p.Selector.Model)
	for _, mode := range p.AllowedModes {
		if route, ok := p.Routes[mode]; ok {
			for _, st := range route.Stages {
				add(st.Model)
			}
		}
	}
	sort.Strings(refs)
	return refs
}

func cloneMapStringAny(m map[string]any) map[string]any {
	if m == nil {
		return nil
	}
	out := make(map[string]any, len(m))
	for k, v := range m {
		out[k] = cloneValueAny(v)
	}
	return out
}

func cloneValueAny(v any) any {
	if v == nil {
		return nil
	}
	return cloneReflectValue(reflect.ValueOf(v)).Interface()
}

func cloneReflectValue(rv reflect.Value) reflect.Value {
	if !rv.IsValid() {
		return rv
	}
	switch rv.Kind() {
	case reflect.Pointer:
		if rv.IsNil() {
			return reflect.Zero(rv.Type())
		}
		elemCopy := cloneReflectValue(rv.Elem())
		ptr := reflect.New(rv.Type().Elem())
		ptr.Elem().Set(elemCopy)
		return ptr
	case reflect.Interface:
		if rv.IsNil() {
			return reflect.Zero(rv.Type())
		}
		return cloneReflectValue(rv.Elem())
	case reflect.Map:
		if rv.IsNil() {
			return reflect.Zero(rv.Type())
		}
		outMap := reflect.MakeMapWithSize(rv.Type(), rv.Len())
		iter := rv.MapRange()
		for iter.Next() {
			kCopy := cloneReflectValue(iter.Key())
			vCopy := cloneReflectValue(iter.Value())
			outMap.SetMapIndex(kCopy, vCopy)
		}
		return outMap
	case reflect.Slice:
		if rv.IsNil() {
			return reflect.Zero(rv.Type())
		}
		outSlice := reflect.MakeSlice(rv.Type(), rv.Len(), rv.Cap())
		for i := 0; i < rv.Len(); i++ {
			elemCopy := cloneReflectValue(rv.Index(i))
			outSlice.Index(i).Set(elemCopy)
		}
		return outSlice
	case reflect.Array:
		outArray := reflect.New(rv.Type()).Elem()
		for i := 0; i < rv.Len(); i++ {
			elemCopy := cloneReflectValue(rv.Index(i))
			outArray.Index(i).Set(elemCopy)
		}
		return outArray
	default:
		return rv
	}
}

// Registered mode descriptors. These are the only mode shapes config recognizes
// at load time.
const (
	ModeDirect = "direct"
	ModeLight  = "light"
)

// SingleRequest absolute caps. These are server-owned upper bounds that no
// operator configuration may exceed. They are intentionally small and fixed so
// a fixed single-request execution cannot consume unbounded resources.
const (
	MaxSingleRequestWallClockMS      = 30 * 60 * 1000 // 30 minutes in milliseconds
	MaxSingleRequestStageTimeoutMS   = 10 * 60 * 1000 // 10 minutes in milliseconds
	MaxSingleRequestToolIterations   = 64
	MaxSingleRequestOutputBytes      = 16 * 1024 * 1024 // 16 MiB
	SingleRequestReasoningEffortHigh = "high"
	singleRequestRequiredStagesCount = 3
)

// singleRequestRequiredStageRoles enumerates the only approved stage roles for
// a fixed single-request preset. The order is plan → work → review.
var singleRequestRequiredStageRoles = []string{"plan", "work", "review"}

// ExecutionSingleRequestPolicy is the typed, immutable fixed single-request
// execution policy. It carries an opaque workspace capability reference, hard
// absolute resource caps, and the approved plan/work/review stage bindings.
type ExecutionSingleRequestPolicy struct {
	// WorkspaceRef is an opaque workspace capability reference. It is never
	// exposed as a raw path, credential, Node id, or endpoint.
	WorkspaceRef string `mapstructure:"workspace_ref" yaml:"workspace_ref,omitempty"`
	// Limits declares absolute caps for the fixed single-request execution.
	Limits ExecutionSingleRequestLimits `mapstructure:"limits" yaml:"limits"`
	// Stages declares the approved fixed stage map: plan, work, review.
	Stages ExecutionSingleRequestStages `mapstructure:"stages" yaml:"stages"`
	// Templates declares optional operator template file paths and internal effective templates.
	Templates ExecutionSingleRequestTemplates `mapstructure:"templates" yaml:"templates,omitempty"`
}

// ExecutionSingleRequestTemplates carries the optional operator-configured
// template paths and the resolved effective templates. A nil path is the only
// omission signal: an explicitly configured value, including an empty or
// whitespace-only one, stays distinguishable from omission so the loader can
// reject it before any filesystem access instead of silently falling back to
// the built-in default.
type ExecutionSingleRequestTemplates struct {
	PlanFile        *string `mapstructure:"plan_file" yaml:"plan_file,omitempty"`
	ReviewFile      *string `mapstructure:"review_file" yaml:"review_file,omitempty"`
	EffectivePlan   string  `mapstructure:"-" yaml:"-"`
	EffectiveReview string  `mapstructure:"-" yaml:"-"`
}

// ExecutionSingleRequestLimits carries server-owned absolute resource caps.
// Every field must be in [1, cap] and timeout_ms must not exceed wall_clock_ms.
type ExecutionSingleRequestLimits struct {
	// WallClockMS is the total wall-clock budget for the request in milliseconds.
	WallClockMS int `mapstructure:"wall_clock_ms" yaml:"wall_clock_ms"`
	// StageTimeoutMS is the per-stage timeout in milliseconds.
	StageTimeoutMS int `mapstructure:"timeout_ms" yaml:"timeout_ms"`
	// MaxToolIterations is the maximum tool iterations allowed per stage.
	MaxToolIterations int `mapstructure:"max_tool_iterations" yaml:"max_tool_iterations"`
	// MaxOutputBytes is the maximum output bytes allowed per stage.
	MaxOutputBytes int `mapstructure:"max_output_bytes" yaml:"max_output_bytes"`
}

// ExecutionSingleRequestStages is the fixed plan/work/review stage map.
// Each field is required and must reference a registered model.
type ExecutionSingleRequestStages struct {
	// Plan is the plan stage model binding with high reasoning.
	Plan ExecutionSingleRequestStageConfig `mapstructure:"plan" yaml:"plan"`
	// Work is the work stage model binding without high reasoning.
	Work ExecutionSingleRequestStageConfig `mapstructure:"work" yaml:"work"`
	// Review is the review stage model binding with high reasoning.
	Review ExecutionSingleRequestStageConfig `mapstructure:"review" yaml:"review"`
}

// ExecutionSingleRequestStageConfig declares one fixed single-request stage
// with its canonical model reference and optional stage-level options.
type ExecutionSingleRequestStageConfig struct {
	// Model is the canonical model reference for this stage.
	Model string `mapstructure:"model" yaml:"model"`
	// Options is the stage-level model options (e.g. reasoning_effort).
	Options map[string]any `mapstructure:"options" yaml:"options,omitempty"`
}

// Clone returns a deep copy of ExecutionSingleRequestPolicy.
func (p *ExecutionSingleRequestPolicy) Clone() *ExecutionSingleRequestPolicy {
	if p == nil {
		return nil
	}
	out := &ExecutionSingleRequestPolicy{
		WorkspaceRef: p.WorkspaceRef,
		Limits:       p.Limits,
		Stages: ExecutionSingleRequestStages{
			Plan:   p.Stages.Plan.Clone(),
			Work:   p.Stages.Work.Clone(),
			Review: p.Stages.Review.Clone(),
		},
		Templates: p.Templates.Clone(),
	}
	return out
}

// Clone returns a deep copy of ExecutionSingleRequestStageConfig.
func (s ExecutionSingleRequestStageConfig) Clone() ExecutionSingleRequestStageConfig {
	out := s
	out.Options = cloneMapStringAny(s.Options)
	return out
}

// Clone returns a deep copy of ExecutionSingleRequestTemplates. The optional
// path cells are copied into fresh pointers so a cloned snapshot never aliases
// the source configuration.
func (t ExecutionSingleRequestTemplates) Clone() ExecutionSingleRequestTemplates {
	out := t
	out.PlanFile = cloneStringPointer(t.PlanFile)
	out.ReviewFile = cloneStringPointer(t.ReviewFile)
	return out
}

func cloneStringPointer(s *string) *string {
	if s == nil {
		return nil
	}
	value := *s
	return &value
}

// ModeDescriptor is the pure shape descriptor for a registered mode.
type ModeDescriptor struct {
	Name           string   `yaml:"-"`
	MaxStages      int      `yaml:"-"`
	RequiredStages []string `yaml:"-"`
	MaxOptions     int      `yaml:"-"`
}

var registeredModeDescriptors = map[string]ModeDescriptor{
	ModeDirect: {
		Name:           ModeDirect,
		MaxStages:      0,
		RequiredStages: []string{},
		MaxOptions:     0,
	},
	ModeLight: {
		Name:           ModeLight,
		MaxStages:      2,
		RequiredStages: []string{"local", "review"},
		MaxOptions:     4,
	},
}

// validatePresetCatalog validates the entire execution preset catalog against structural
// rules and canonical model IDs.
func validatePresetCatalog(presets []ExecutionPreset, canonicalModelIDs map[string]struct{}) error {
	seenIDs := make(map[string]struct{}, len(presets))
	for i := range presets {
		p := &presets[i]
		if err := validatePreset(i, p, seenIDs, canonicalModelIDs); err != nil {
			return err
		}
	}
	return nil
}

func validatePreset(index int, p *ExecutionPreset, seenIDs map[string]struct{}, canonicalModelIDs map[string]struct{}) error {
	p.ID = strings.TrimSpace(p.ID)
	if p.ID == "" {
		return fmt.Errorf("execution_presets[%d]: id must not be empty", index)
	}
	if _, dup := seenIDs[p.ID]; dup {
		return fmt.Errorf("execution_presets[%d]: duplicate preset id %q", index, p.ID)
	}
	seenIDs[p.ID] = struct{}{}

	// Validate & normalize selector model
	p.Selector.Model = strings.TrimSpace(p.Selector.Model)
	if p.Selector.Model == "" {
		return fmt.Errorf("execution_presets[%d] id=%q: selector model must not be empty", index, p.ID)
	}
	if _, ok := canonicalModelIDs[p.Selector.Model]; !ok {
		return fmt.Errorf("execution_presets[%d] id=%q: selector model %q not found in models catalog", index, p.ID, p.Selector.Model)
	}

	// Validate & normalize allowed modes
	if len(p.AllowedModes) == 0 {
		return fmt.Errorf("execution_presets[%d] id=%q: allowed_modes must not be empty", index, p.ID)
	}
	seenModes := make(map[string]struct{}, len(p.AllowedModes))
	for j, mode := range p.AllowedModes {
		m := strings.TrimSpace(mode)
		if m == "" {
			return fmt.Errorf("execution_presets[%d] id=%q: allowed_modes[%d] must not be empty", index, p.ID, j)
		}
		if _, dup := seenModes[m]; dup {
			return fmt.Errorf("execution_presets[%d] id=%q: duplicate allowed mode %q", index, p.ID, m)
		}
		seenModes[m] = struct{}{}
		if _, ok := registeredModeDescriptors[m]; !ok {
			return fmt.Errorf("execution_presets[%d] id=%q: allowed_modes[%d] %q is not a registered mode descriptor (allowed: %s)",
				index, p.ID, j, m, registeredModeDescriptorNames())
		}
		p.AllowedModes[j] = m
	}

	// Validate routes match allowed_modes exactly
	if p.Routes == nil {
		return fmt.Errorf("execution_presets[%d] id=%q: routes must be defined", index, p.ID)
	}
	normalizedRoutes := make(map[string]ExecutionRoute, len(p.Routes))
	for _, rawKey := range sortedRouteKeys(p.Routes) {
		mode := strings.TrimSpace(rawKey)
		if mode == "" {
			return fmt.Errorf("execution_presets[%d] id=%q: route key must not be empty", index, p.ID)
		}
		if _, duplicate := normalizedRoutes[mode]; duplicate {
			return fmt.Errorf("execution_presets[%d] id=%q: duplicate route key %q after normalization", index, p.ID, mode)
		}
		normalizedRoutes[mode] = p.Routes[rawKey]
	}
	p.Routes = normalizedRoutes

	for _, m := range p.AllowedModes {
		if _, ok := p.Routes[m]; !ok {
			return fmt.Errorf("execution_presets[%d] id=%q: missing route for allowed mode %q", index, p.ID, m)
		}
	}
	for _, rKey := range sortedRouteKeys(p.Routes) {
		if _, ok := seenModes[rKey]; !ok {
			return fmt.Errorf("execution_presets[%d] id=%q: route key %q is not in allowed_modes", index, p.ID, rKey)
		}
	}

	// Validate each route in allowed_modes order.
	// When SingleRequest is set, skip the standard light-mode route validation
	// (MaxStages/RequiredStages) because the single-request policy enforces its
	// own approved plan→work→review shape.
	for _, m := range p.AllowedModes {
		// For single-request presets, skip standard route validation on light.
		if p.SingleRequest != nil && m == ModeLight {
			continue
		}
		route := p.Routes[m]
		desc := registeredModeDescriptors[m]
		if err := validatePresetRoute(index, p.ID, m, &route, desc, canonicalModelIDs); err != nil {
			return err
		}
		p.Routes[m] = route
	}

	// Validate workspace tools (skipped for single-request presets which
	// reject legacy caller workspace_tools by design).
	if p.SingleRequest != nil && len(p.WorkspaceTools) > 0 {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request preset must not declare workspace_tools",
			index, p.ID)
	}
	if p.SingleRequest == nil {
		if err := validateWorkspaceTools(index, p.ID, p.WorkspaceTools, seenModes); err != nil {
			return err
		}
	}

	// Validate fixed single-request policy if present.
	if p.SingleRequest != nil {
		if err := validateSingleRequestPolicy(index, p, seenModes, canonicalModelIDs); err != nil {
			return err
		}
	}

	return nil
}

func validatePresetRoute(presetIndex int, presetID string, mode string, route *ExecutionRoute, desc ModeDescriptor, canonicalModelIDs map[string]struct{}) error {
	if desc.MaxStages == 0 {
		if len(route.Stages) > 0 {
			return fmt.Errorf("execution_presets[%d] id=%q: mode %q declares no downstream stages, got %d",
				presetIndex, presetID, mode, len(route.Stages))
		}
		return nil
	}

	if len(route.Stages) > desc.MaxStages {
		return fmt.Errorf("execution_presets[%d] id=%q: mode %q allows at most %d route stages, got %d",
			presetIndex, presetID, mode, desc.MaxStages, len(route.Stages))
	}

	// Enforce option bounds on ALL stages before checking roles/required stages
	for i := range route.Stages {
		st := &route.Stages[i]
		st.Role = strings.TrimSpace(st.Role)
		st.Model = strings.TrimSpace(st.Model)
		if st.Role == "" {
			return fmt.Errorf("execution_presets[%d] id=%q: mode %q stage[%d]: role must not be empty",
				presetIndex, presetID, mode, i)
		}
		if desc.MaxOptions > 0 && len(st.Options) > desc.MaxOptions {
			return fmt.Errorf("execution_presets[%d] id=%q: mode %q stage[%d] role=%q allows at most %d options, got %d",
				presetIndex, presetID, mode, i, st.Role, desc.MaxOptions, len(st.Options))
		}
		if st.Model == "" {
			return fmt.Errorf("execution_presets[%d] id=%q: mode %q stage[%d]: model must not be empty",
				presetIndex, presetID, mode, i)
		}
		if _, ok := canonicalModelIDs[st.Model]; !ok {
			return fmt.Errorf("execution_presets[%d] id=%q: mode %q stage[%d]: model %q not found in models catalog",
				presetIndex, presetID, mode, i, st.Model)
		}
	}

	// Enforce required stages and exact order
	if len(desc.RequiredStages) > 0 {
		if len(route.Stages) != len(desc.RequiredStages) {
			return fmt.Errorf("execution_presets[%d] id=%q: mode %q requires stages [%s], got %d stages",
				presetIndex, presetID, mode, strings.Join(desc.RequiredStages, ","), len(route.Stages))
		}
		for i, reqRole := range desc.RequiredStages {
			if route.Stages[i].Role != reqRole {
				return fmt.Errorf("execution_presets[%d] id=%q: mode %q stage[%d] role is %q, want %q",
					presetIndex, presetID, mode, i, route.Stages[i].Role, reqRole)
			}
		}
	}

	return nil
}

func validateWorkspaceTools(presetIndex int, presetID string, tools []ExecutionWorkspaceToolAlternative, allowedModes map[string]struct{}) error {
	if _, light := allowedModes[ModeLight]; light && len(tools) == 0 {
		return fmt.Errorf("execution_presets[%d] id=%q: mode %q requires at least one workspace_tools alternative",
			presetIndex, presetID, ModeLight)
	}

	seenAltNames := make(map[string]struct{}, len(tools))
	for j := range tools {
		alt := &tools[j]
		alt.Name = strings.TrimSpace(alt.Name)
		if alt.Name == "" {
			return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d]: name must not be empty",
				presetIndex, presetID, j)
		}
		if _, dup := seenAltNames[alt.Name]; dup {
			return fmt.Errorf("execution_presets[%d] id=%q: duplicate workspace_tools alternative name %q",
				presetIndex, presetID, alt.Name)
		}
		seenAltNames[alt.Name] = struct{}{}

		if alt.Operations == nil {
			return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: operations must be defined",
				presetIndex, presetID, j, alt.Name)
		}

		normalizedOps := make(map[string]ExecutionWorkspaceOperation, len(alt.Operations))
		opNames := make([]string, 0, len(alt.Operations))
		for opName := range alt.Operations {
			opNames = append(opNames, opName)
		}
		sort.Strings(opNames)

		for _, rawOp := range opNames {
			op := alt.Operations[rawOp]
			trimmedOp := strings.TrimSpace(rawOp)
			if trimmedOp != "prepare" && trimmedOp != "read" && trimmedOp != "write" && trimmedOp != "delete" {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: unknown operation %q",
					presetIndex, presetID, j, alt.Name, rawOp)
			}
			if _, dup := normalizedOps[trimmedOp]; dup {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: duplicate operation %q",
					presetIndex, presetID, j, alt.Name, trimmedOp)
			}
			op.ToolName = strings.TrimSpace(op.ToolName)
			if op.ToolName == "" {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: operation %q tool_name must not be empty",
					presetIndex, presetID, j, alt.Name, trimmedOp)
			}
			if op.SchemaMatcher == nil || len(op.SchemaMatcher) == 0 {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: operation %q schema_matcher must not be empty",
					presetIndex, presetID, j, alt.Name, trimmedOp)
			}
			if op.ArgumentMap == nil || len(op.ArgumentMap) == 0 {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: operation %q argument_map must not be empty",
					presetIndex, presetID, j, alt.Name, trimmedOp)
			}
			if op.ResultMatcher == nil || len(op.ResultMatcher) == 0 {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: operation %q result_matcher must not be empty",
					presetIndex, presetID, j, alt.Name, trimmedOp)
			}
			normalizedOps[trimmedOp] = op
		}
		alt.Operations = normalizedOps

		if _, permitsLight := allowedModes[ModeLight]; permitsLight {
			if _, hasRead := alt.Operations["read"]; !hasRead {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: mode %q requires operation %q",
					presetIndex, presetID, j, alt.Name, ModeLight, "read")
			}
			writeOp, hasWrite := alt.Operations["write"]
			if !hasWrite {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: mode %q requires operation %q",
					presetIndex, presetID, j, alt.Name, ModeLight, "write")
			}
			if _, hasDelete := alt.Operations["delete"]; !hasDelete {
				return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: mode %q requires operation %q",
					presetIndex, presetID, j, alt.Name, ModeLight, "delete")
			}
			if !writeOp.CreatesParents {
				if _, hasPrep := alt.Operations["prepare"]; !hasPrep {
					return fmt.Errorf("execution_presets[%d] id=%q: workspace_tools[%d] name=%q: operation \"write\" does not create parents, so \"prepare\" operation is required",
						presetIndex, presetID, j, alt.Name)
				}
			}
		}
	}
	return nil
}

// validateSingleRequestPolicy enforces the approved fixed single-request shape:
// opaque workspace_ref, absolute resource caps in [1, cap] with stage_timeout
// not exceeding wall_clock, exactly plan/work/review stages with high-reasoning
// on selector and review, no high-reasoning on work, and no legacy workspace_tools.
func validateSingleRequestPolicy(presetIndex int, p *ExecutionPreset, allowedModes map[string]struct{}, canonicalModelIDs map[string]struct{}) error {
	sr := p.SingleRequest

	// WorkspaceRef must be a non-empty opaque reference.
	sr.WorkspaceRef = strings.TrimSpace(sr.WorkspaceRef)
	if sr.WorkspaceRef == "" {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.workspace_ref must not be empty", presetIndex, p.ID)
	}

	// Limits validation: every field in [1, cap], stage_timeout <= wall_clock.
	l := &sr.Limits
	if l.WallClockMS < 1 || l.WallClockMS > MaxSingleRequestWallClockMS {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.limits.wall_clock_ms must be in [1, %d], got %d",
			presetIndex, p.ID, MaxSingleRequestWallClockMS, l.WallClockMS)
	}
	if l.StageTimeoutMS < 1 || l.StageTimeoutMS > MaxSingleRequestStageTimeoutMS {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.limits.timeout_ms must be in [1, %d], got %d",
			presetIndex, p.ID, MaxSingleRequestStageTimeoutMS, l.StageTimeoutMS)
	}
	if l.StageTimeoutMS > l.WallClockMS {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.limits.timeout_ms (%d) must not exceed wall_clock_ms (%d)",
			presetIndex, p.ID, l.StageTimeoutMS, l.WallClockMS)
	}
	if l.MaxToolIterations < 1 || l.MaxToolIterations > MaxSingleRequestToolIterations {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.limits.max_tool_iterations must be in [1, %d], got %d",
			presetIndex, p.ID, MaxSingleRequestToolIterations, l.MaxToolIterations)
	}
	if l.MaxOutputBytes < 1 || l.MaxOutputBytes > MaxSingleRequestOutputBytes {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.limits.max_output_bytes must be in [1, %d], got %d",
			presetIndex, p.ID, MaxSingleRequestOutputBytes, l.MaxOutputBytes)
	}

	// Stages must contain exactly plan, work, review.
	stages := sr.Stages
	if stages.Plan.Model == "" {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.stages.plan.model must not be empty", presetIndex, p.ID)
	}
	if stages.Work.Model == "" {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.stages.work.model must not be empty", presetIndex, p.ID)
	}
	if stages.Review.Model == "" {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.stages.review.model must not be empty", presetIndex, p.ID)
	}

	// Each stage model must be in the canonical model catalog.
	for _, role := range singleRequestRequiredStageRoles {
		var model string
		switch role {
		case "plan":
			model = stages.Plan.Model
		case "work":
			model = stages.Work.Model
		case "review":
			model = stages.Review.Model
		}
		if _, ok := canonicalModelIDs[model]; !ok {
			return fmt.Errorf("execution_presets[%d] id=%q: single_request.stages.%s.model %q not found in models catalog",
				presetIndex, p.ID, role, model)
		}
	}

	// Selector must have high reasoning effort.
	selectorEffort := getReasoningEffort(p.Selector.Options)
	if selectorEffort != SingleRequestReasoningEffortHigh {
		return fmt.Errorf("execution_presets[%d] id=%q: selector.options must have reasoning_effort=%q, got %q",
			presetIndex, p.ID, SingleRequestReasoningEffortHigh, selectorEffort)
	}

	// Plan stage must have high reasoning effort.
	planEffort := getReasoningEffort(stages.Plan.Options)
	if planEffort != SingleRequestReasoningEffortHigh {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.stages.plan.options must have reasoning_effort=%q, got %q",
			presetIndex, p.ID, SingleRequestReasoningEffortHigh, planEffort)
	}

	// Selector binding must match single_request plan stage.
	if p.Selector.Model != stages.Plan.Model || !optionsEqual(p.Selector.Options, stages.Plan.Options) {
		return fmt.Errorf("execution_presets[%d] id=%q: selector binding must match single_request plan stage", presetIndex, p.ID)
	}

	// Review stage must have high reasoning; work stage must not declare reasoning_effort.
	reviewEffort := getReasoningEffort(stages.Review.Options)
	if reviewEffort != SingleRequestReasoningEffortHigh {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.stages.review.options must have reasoning_effort=%q, got %q",
			presetIndex, p.ID, SingleRequestReasoningEffortHigh, reviewEffort)
	}
	if _, present := stages.Work.Options["reasoning_effort"]; present {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request.stages.work.options must not declare reasoning_effort", presetIndex, p.ID)
	}

	// Allowed modes must be exactly ["light"].
	if len(p.AllowedModes) != 1 || p.AllowedModes[0] != ModeLight {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request preset allowed_modes must be exactly [%q], got %v",
			presetIndex, p.ID, ModeLight, p.AllowedModes)
	}

	// Routes must declare exactly one "light" mode with plan→work→review stages matching policy stages.
	route, hasRoute := p.Routes[ModeLight]
	if !hasRoute {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request preset must declare a route for mode %q", presetIndex, p.ID, ModeLight)
	}
	if len(route.Stages) != singleRequestRequiredStagesCount {
		return fmt.Errorf("execution_presets[%d] id=%q: single_request preset light route must have exactly %d stages, got %d",
			presetIndex, p.ID, singleRequestRequiredStagesCount, len(route.Stages))
	}
	expectedStages := []ExecutionSingleRequestStageConfig{
		stages.Plan,
		stages.Work,
		stages.Review,
	}
	for i, expectedRole := range singleRequestRequiredStageRoles {
		routeStage := route.Stages[i]
		if routeStage.Role != expectedRole {
			return fmt.Errorf("execution_presets[%d] id=%q: single_request preset light route stage[%d] role is %q, want %q",
				presetIndex, p.ID, i, routeStage.Role, expectedRole)
		}
		if _, ok := canonicalModelIDs[routeStage.Model]; !ok {
			return fmt.Errorf("execution_presets[%d] id=%q: single_request preset light route stage[%d] role=%q model %q not found in models catalog",
				presetIndex, p.ID, i, routeStage.Role, routeStage.Model)
		}
		if routeStage.Model != expectedStages[i].Model || !optionsEqual(routeStage.Options, expectedStages[i].Options) {
			return fmt.Errorf("execution_presets[%d] id=%q: single_request preset light route stage[%d] role=%q binding must match single_request stage",
				presetIndex, p.ID, i, routeStage.Role)
		}
	}

	if sr.Templates.EffectivePlan != "" {
		if err := singlerequesttemplate.ValidatePlanTemplate(sr.Templates.EffectivePlan); err != nil {
			return fmt.Errorf("execution_presets[%d] id=%q: invalid single_request.templates.plan_file: %w", presetIndex, p.ID, err)
		}
	}
	if sr.Templates.EffectiveReview != "" {
		if err := singlerequesttemplate.ValidateReviewTemplate(sr.Templates.EffectiveReview); err != nil {
			return fmt.Errorf("execution_presets[%d] id=%q: invalid single_request.templates.review_file: %w", presetIndex, p.ID, err)
		}
	}

	return nil
}

func optionsEqual(a, b map[string]any) bool {
	if len(a) == 0 && len(b) == 0 {
		return true
	}
	return reflect.DeepEqual(a, b)
}

// getReasoningEffort extracts the reasoning_effort option value from a stage's
// options map, returning "" when absent or non-string.
func getReasoningEffort(opts map[string]any) string {
	if opts == nil {
		return ""
	}
	v, ok := opts["reasoning_effort"]
	if !ok {
		return ""
	}
	s, ok := v.(string)
	if !ok {
		return ""
	}
	return s
}

func registeredModeDescriptorNames() string {
	names := make([]string, 0, len(registeredModeDescriptors))
	for name := range registeredModeDescriptors {
		names = append(names, name)
	}
	sort.Strings(names)
	return strings.Join(names, ",")
}

func sortedRouteKeys(routes map[string]ExecutionRoute) []string {
	keys := make([]string, 0, len(routes))
	for k := range routes {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}
