package plan

import (
	"fmt"
	"math"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

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

var (
	idPattern        = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,63}$`)
	errorCodePattern = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$`)
	templatePattern  = regexp.MustCompile(`\{\{\s*([^{}]+?)\s*\}\}`)
)

const maxSchemaArrayDepth = 32

type validator struct {
	document       *Document
	source         *sourceIndex
	diagnostics    []Diagnostic
	nodes          map[string]*Node
	nodeIndexes    map[string]int
	edges          map[string]*Edge
	edgeIndexes    map[string]int
	parallelScopes []parallelScopeAnalysis
	loopScopes     []loopScopeAnalysis
	fanOutScopes   []fanOutScopeAnalysis
}

// Validate validates an already parsed document. Compile additionally adds
// source coordinates and produces the immutable artifact.
func Validate(document *Document) []Diagnostic {
	diagnostics, _ := validate(document, nil)
	return diagnostics
}

type compilerAnalysis struct {
	parallelScopes []parallelScopeAnalysis
	loopScopes     []loopScopeAnalysis
	fanOutScopes   []fanOutScopeAnalysis
}

func validate(document *Document, source *sourceIndex) ([]Diagnostic, compilerAnalysis) {
	if document == nil {
		return nil, compilerAnalysis{}
	}
	if diagnostics := documentBudgetDiagnostics(document); len(diagnostics) != 0 {
		if source != nil {
			for index := range diagnostics {
				diagnostics[index] = source.decorate(diagnostics[index])
			}
		}
		sortDiagnostics(diagnostics)
		return diagnostics, compilerAnalysis{}
	}
	if diagnostics := implicitFailureBudgetDiagnostics(document); len(diagnostics) != 0 {
		if source != nil {
			for index := range diagnostics {
				diagnostics[index] = source.decorate(diagnostics[index])
			}
		}
		sortDiagnostics(diagnostics)
		return diagnostics, compilerAnalysis{}
	}
	v := &validator{
		document: document, source: source,
		nodes: map[string]*Node{}, nodeIndexes: map[string]int{},
		edges: map[string]*Edge{}, edgeIndexes: map[string]int{},
	}
	v.validateHeaderAndSchemas()
	v.indexGraph()
	v.validateNodes()
	v.validateGraph()
	analysis := compilerAnalysis{parallelScopes: v.parallelScopes, loopScopes: v.loopScopes, fanOutScopes: v.fanOutScopes}
	if diagnostics := artifactMaterializationBudgetDiagnostics(document, &analysis); len(diagnostics) != 0 {
		v.diagnostics = append(v.diagnostics, diagnostics...)
	}
	if source != nil {
		for i := range v.diagnostics {
			v.diagnostics[i] = source.decorate(v.diagnostics[i])
		}
	}
	sortDiagnostics(v.diagnostics)
	sort.Slice(v.parallelScopes, func(i, j int) bool { return v.parallelScopes[i].ownerID < v.parallelScopes[j].ownerID })
	sort.Slice(v.loopScopes, func(i, j int) bool { return v.loopScopes[i].ownerID < v.loopScopes[j].ownerID })
	sort.Slice(v.fanOutScopes, func(i, j int) bool { return v.fanOutScopes[i].ownerID < v.fanOutScopes[j].ownerID })
	return v.diagnostics, analysis
}

func (v *validator) error(code, path, message, nodeID, edgeID string) {
	v.diagnostics = append(v.diagnostics, Diagnostic{
		Severity: SeverityError, Code: code, Path: path, Message: message,
		NodeID: nodeID, EdgeID: edgeID,
	})
}

func (v *validator) validateHeaderAndSchemas() {
	if v.document.APIVersion != APIVersion {
		v.error("header.api_version", "apiVersion", fmt.Sprintf("apiVersion must be %q", APIVersion), "", "")
	}
	if v.document.Kind != Kind {
		v.error("header.kind", "kind", fmt.Sprintf("kind must be %q", Kind), "", "")
	}
	v.validateSchemaMap("input", v.document.Input, "")
	v.validateSchemaMap("output", v.document.Output, "")
	v.validateSchemaMap("state", v.document.State, "")
	v.rejectAuthoredWorkItemsSchemas("input", v.document.Input, "")
	v.rejectAuthoredWorkItemsSchemas("output", v.document.Output, "")
	v.rejectAuthoredWorkItemsSchemas("state", v.document.State, "")
}

func (v *validator) validateSchemaMap(path string, schemas map[string]Schema, nodeID string) {
	for _, name := range sortedSchemaKeys(schemas) {
		fieldPath := path + "." + name
		if !idPattern.MatchString(name) &&
			!(strings.HasSuffix(path, ".sendMessage.callbackOutput") && isSendMessageSystemOutput(name)) &&
			!(strings.Contains(path, ".input") && isWorkItemContractField(name)) {
			v.error("schema.invalid_field_id", fieldPath, "field name must match "+idPattern.String(), nodeID, "")
		}
		v.validateSchema(fieldPath, schemas[name], nodeID, 0)
	}
}

func (v *validator) validateSchema(path string, schema Schema, nodeID string, depth int) {
	if depth > maxSchemaArrayDepth {
		v.error("schema.items_too_deep", path, fmt.Sprintf("array items exceed the maximum nesting depth of %d", maxSchemaArrayDepth), nodeID, "")
		return
	}
	if schema.Type == "" {
		v.error("schema.type_required", path+".type", "type is required", nodeID, "")
	} else if !authoringContains(compilerAuthoringCapabilities.ValueTypes, schema.Type) {
		v.error("schema.unknown_type", path+".type", fmt.Sprintf("unsupported type %q", schema.Type), nodeID, "")
	} else {
		switch schema.Type {
		case TypeEnum:
			if len(schema.Values) == 0 {
				v.error("schema.enum_values_required", path+".values", "enum requires at least one value", nodeID, "")
			}
		case TypeArray:
			if schema.Items == nil {
				v.error("schema.items_required", path+".items", "array requires an items schema", nodeID, "")
			} else {
				v.validateSchema(path+".items", *schema.Items, nodeID, depth+1)
			}
		}
	}
	if schema.Type != TypeEnum && len(schema.Values) != 0 {
		v.error("schema.values_not_allowed", path+".values", "values is only valid for enum", nodeID, "")
	}
	if schema.Type != TypeArray && schema.Items != nil {
		v.error("schema.items_not_allowed", path+".items", "items is only valid for array", nodeID, "")
	}
	if schema.Default != nil {
		v.validateLiteral(path+".default", schema.Default, nodeID, "")
		if !valueMatchesSchema(schema.Default, schema) {
			v.error("schema.default_type", path+".default", "default does not match the declared type", nodeID, "")
		}
	}
}

func sortedSchemaKeys(values map[string]Schema) []string {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func valueMatchesSchema(value any, schema Schema) bool {
	switch schema.Type {
	case TypeChannel, TypeString, TypeMember, TypeAgent:
		_, ok := value.(string)
		return ok
	case TypeResponder:
		text, ok := value.(string)
		if ok {
			return strings.TrimSpace(text) != ""
		}
		object, ok := value.(map[string]any)
		if !ok {
			return false
		}
		raw, present := object["selectOne"]
		if !present || len(object) != 1 {
			return false
		}
		values, ok := raw.([]any)
		if !ok || len(values) == 0 || len(values) > MaxAllowedResponders {
			return false
		}
		for _, rawCandidate := range values {
			candidate, candidateOK := rawCandidate.(string)
			if !candidateOK || strings.TrimSpace(candidate) == "" {
				return false
			}
		}
		return true
	case TypeScope:
		_, err := domain.NormalizeMessageScopeValue(value)
		return err == nil
	case TypeDuration:
		text, ok := value.(string)
		if !ok {
			return false
		}
		duration, err := parsePlanDuration(text)
		return err == nil && duration > 0
	case TypeDatetime:
		text, ok := value.(string)
		if !ok {
			return false
		}
		_, err := time.Parse(time.RFC3339, text)
		return err == nil
	case TypeEnum:
		text, ok := value.(string)
		if !ok {
			return false
		}
		for _, allowed := range schema.Values {
			if text == allowed {
				return true
			}
		}
		return false
	case TypeNumber:
		_, ok := numericValue(value)
		return ok
	case TypeBoolean:
		_, ok := value.(bool)
		return ok
	case TypeObject:
		_, ok := value.(map[string]any)
		return ok
	case TypeArray:
		values, ok := value.([]any)
		if !ok || schema.Items == nil {
			return ok
		}
		for _, item := range values {
			if !valueMatchesSchema(item, *schema.Items) {
				return false
			}
		}
		return true
	default:
		return false
	}
}

func numericValue(value any) (float64, bool) {
	switch number := value.(type) {
	case int:
		return float64(number), true
	case int8:
		return float64(number), true
	case int16:
		return float64(number), true
	case int32:
		return float64(number), true
	case int64:
		return float64(number), true
	case uint:
		return float64(number), true
	case uint8:
		return float64(number), true
	case uint16:
		return float64(number), true
	case uint32:
		return float64(number), true
	case uint64:
		return float64(number), true
	case float32:
		converted := float64(number)
		return converted, !math.IsNaN(converted) && !math.IsInf(converted, 0)
	case float64:
		return number, !math.IsNaN(number) && !math.IsInf(number, 0)
	default:
		return 0, false
	}
}

func (v *validator) indexGraph() {
	for i := range v.document.Graph.Nodes {
		node := &v.document.Graph.Nodes[i]
		path := fmt.Sprintf("graph.nodes[%d].id", i)
		if node.ID == "" {
			v.error("graph.node_id_required", path, "node id is required", "", "")
			continue
		}
		if !idPattern.MatchString(node.ID) {
			v.error("graph.invalid_node_id", path, "node id must match "+idPattern.String(), node.ID, "")
		}
		if usesImplicitFailureAuthoring(v.document) && strings.HasPrefix(node.ID, implicitFailureIDPrefix) {
			v.error("graph.reserved_node_id", path, fmt.Sprintf("node id prefix %q is reserved for compiler-generated failure sinks", implicitFailureIDPrefix), node.ID, "")
		}
		if _, exists := v.nodes[node.ID]; exists {
			v.error("graph.duplicate_node_id", path, fmt.Sprintf("duplicate node id %q", node.ID), node.ID, "")
			continue
		}
		v.nodes[node.ID], v.nodeIndexes[node.ID] = node, i
	}
	for i := range v.document.Graph.Edges {
		edge := &v.document.Graph.Edges[i]
		path := fmt.Sprintf("graph.edges[%d].id", i)
		if edge.ID == "" {
			v.error("graph.edge_id_required", path, "edge id is required", "", "")
			continue
		}
		if !idPattern.MatchString(edge.ID) {
			v.error("graph.invalid_edge_id", path, "edge id must match "+idPattern.String(), "", edge.ID)
		}
		if usesImplicitFailureAuthoring(v.document) && strings.HasPrefix(edge.ID, implicitFailureIDPrefix) {
			v.error("graph.reserved_edge_id", path, fmt.Sprintf("edge id prefix %q is reserved for compiler-generated failure routing", implicitFailureIDPrefix), "", edge.ID)
		}
		if _, exists := v.edges[edge.ID]; exists {
			v.error("graph.duplicate_edge_id", path, fmt.Sprintf("duplicate edge id %q", edge.ID), "", edge.ID)
			continue
		}
		v.edges[edge.ID], v.edgeIndexes[edge.ID] = edge, i
	}
}

func (v *validator) validateNodes() {
	for i := range v.document.Graph.Nodes {
		node := &v.document.Graph.Nodes[i]
		path := fmt.Sprintf("graph.nodes[%d]", i)
		v.validateSchemaMap(path+".input", node.Input, node.ID)
		v.validateNodeWorkItemsInput(path, node)
		v.validateNodeConfig(path, node)
	}
}

func (v *validator) validateNodeConfig(path string, node *Node) {
	expected, configs, supported := compilerAuthoringCapabilities.nodeConfig(node)
	if !supported {
		v.error("node.unknown_type", path+".type", fmt.Sprintf("unsupported node type %q", node.Type), node.ID, "")
		return
	}
	for name, present := range configs {
		if present && name != expected {
			v.error("node.unexpected_config", path+"."+name, fmt.Sprintf("%s config is invalid for type %q", name, node.Type), node.ID, "")
		}
	}
	if !configs[expected] {
		v.error("node.config_required", path+"."+expected, fmt.Sprintf("type %q requires %s config", node.Type, expected), node.ID, "")
		return
	}

	switch node.Type {
	case NodeSendMessage:
		v.validateSendMessageConfig(path, node)
	case NodeSetVariable:
		v.validateAssignments(path, node)
	case NodeWait:
		untilSchema := v.validateValueExpr(path+".wait.until", node.Wait.Until, node, referenceContext{}, "")
		if untilSchema.Type != "" && !waitUntilAssignable(node.Wait.Until, untilSchema) {
			v.error("wait.until_type", path+".wait.until", "until must resolve to datetime or duration", node.ID, "")
		}
		v.validateDuration(path+".wait.timeout", node.Wait.Timeout, node.ID)
		v.validateRetry(path+".wait.retry", effectiveActionRetry(v.document, node.Wait.Retry), node.ID)
	case NodeRequestApproval:
		if strings.TrimSpace(node.RequestApproval.Assignee) == "" {
			v.error("approval.assignee_required", path+".requestApproval.assignee", "assignee is required", node.ID, "")
		}
		if strings.TrimSpace(node.RequestApproval.Summary) == "" {
			v.error("approval.summary_required", path+".requestApproval.summary", "summary is required", node.ID, "")
		}
		v.validateTemplate(path+".requestApproval.assignee", node.RequestApproval.Assignee, node)
		v.validateTemplate(path+".requestApproval.summary", node.RequestApproval.Summary, node)
		if node.RequestApproval.hasFeedbackScope() && node.RequestApproval.FeedbackScope == nil {
			v.error("approval.feedback_scope_required", path+".requestApproval.feedbackScope", "feedbackScope must be a value expression when present; omit feedbackScope to retain the legacy approval contract", node.ID, "")
		}
		if node.RequestApproval.hasFeedbackScope() && node.RequestApproval.FeedbackScope != nil {
			feedbackScopeSchema := v.validateValueExpr(
				path+".requestApproval.feedbackScope",
				*node.RequestApproval.FeedbackScope,
				node,
				referenceContext{activationOutputUnavailable: true},
				"",
			)
			requiredScope := guaranteedSchema(Schema{Type: TypeScope})
			if feedbackScopeSchema.Type != "" && !valueExprAssignable(*node.RequestApproval.FeedbackScope, feedbackScopeSchema, requiredScope) {
				v.error("approval.feedback_scope_type", path+".requestApproval.feedbackScope", "feedbackScope must resolve to a required canonical scope value", node.ID, "")
			}
		}
		v.validateRetry(path+".requestApproval.retry", effectiveActionRetry(v.document, node.RequestApproval.Retry), node.ID)
	case NodeSwitch:
		v.validateSwitch(path, node)
	case NodeFork:
		v.validateFork(path, node)
	case NodeFanOut:
		itemsSchema := v.validateValueExpr(path+".fanOut.items", node.FanOut.Items, node, referenceContext{}, "")
		requiredWorkItems := guaranteedSchema(Schema{Type: TypeWorkItems})
		if itemsSchema.Type != "" && !valueExprAssignable(node.FanOut.Items, itemsSchema, requiredWorkItems) {
			v.error("fan_out.items_type", path+".fanOut.items", "items must resolve to a required work_items value", node.ID, "")
		}
		if node.FanOut.MaxConcurrency != nil {
			if *node.FanOut.MaxConcurrency < 1 {
				v.error("fan_out.invalid_max_concurrency", path+".fanOut.maxConcurrency", "maxConcurrency must be at least 1", node.ID, "")
			} else if *node.FanOut.MaxConcurrency > MaxFanOutConcurrency {
				v.error("fan_out.max_concurrency_too_large", path+".fanOut.maxConcurrency", fmt.Sprintf("maxConcurrency must not exceed %d", MaxFanOutConcurrency), node.ID, "")
			}
		}
		if !authoringMapContains(compilerAuthoringCapabilities.FanOutFailureModes, node.FanOut.FailureMode) {
			v.error("fan_out.invalid_failure_mode", path+".fanOut.failureMode", "failureMode must be collect", node.ID, "")
		}
	case NodeJoin:
		hasFork, hasFanOut := node.Join.ForkID != "", node.Join.FanOutID != ""
		if hasFork == hasFanOut {
			v.error("join.owner_required", path+".join", "exactly one of forkId or fanOutId is required", node.ID, "")
		}
		if hasFanOut && node.Join.Mode != JoinAll {
			v.error("join.fan_out_mode", path+".join.mode", "a fanOutId join requires mode all", node.ID, "")
		}
		if !authoringMapContains(compilerAuthoringCapabilities.JoinModes, node.Join.Mode) {
			v.error("join.invalid_mode", path+".join.mode", "mode must be all or any", node.ID, "")
		}
		v.validateSchemaMap(path+".join.result", node.Join.Result, node.ID)
		v.rejectAuthoredWorkItemsSchemas(path+".join.result", node.Join.Result, node.ID)
		v.validateBindings(path+".join.merge", node.Join.Merge, node, referenceContext{joinMerge: true}, node.Join.Result, node.ID, "")
	case NodeLoop:
		if len(node.Input) != 0 {
			v.error("loop.input_not_allowed", path+".input", "loop input is declared by its enter/continue state contract", node.ID, "")
		}
		v.validateLoop(path, node)
	case NodeEnd:
		if len(node.Input) != 0 {
			v.error("end.input_not_allowed", path+".input", "end input is defined by the Plan output or failure contract", node.ID, "")
		}
		if !authoringMapContains(compilerAuthoringCapabilities.EndOutcomes, node.End.Outcome) {
			v.error("end.invalid_outcome", path+".end.outcome", "outcome must be succeeded or failed", node.ID, "")
		}
		switch node.End.Outcome {
		case EndFailed:
			if strings.TrimSpace(node.End.Code) == "" {
				v.error("end.code_required", path+".end.code", "failed end requires a stable error code", node.ID, "")
			} else if len(node.End.Code) > 128 || !errorCodePattern.MatchString(node.End.Code) {
				v.error("end.invalid_code", path+".end.code", "error code must be at most 128 characters and match "+errorCodePattern.String(), node.ID, "")
			}
		case EndSucceeded:
			if node.End.Code != "" {
				v.error("end.code_not_allowed", path+".end.code", "succeeded end cannot declare an error code", node.ID, "")
			}
		}
	}
}

func (v *validator) validateAssignments(path string, node *Node) {
	if len(node.SetVariable.Assignments) == 0 {
		v.error("set_variable.assignments_required", path+".setVariable.assignments", "at least one assignment is required", node.ID, "")
	}
	seen := map[string]bool{}
	for i, assignment := range node.SetVariable.Assignments {
		assignmentPath := fmt.Sprintf("%s.setVariable.assignments[%d]", path, i)
		stateSchema, exists := v.document.State[assignment.State]
		if !exists {
			v.error("set_variable.unknown_state", assignmentPath+".state", fmt.Sprintf("state %q is not declared", assignment.State), node.ID, "")
		}
		if seen[assignment.State] {
			v.error("set_variable.duplicate_assignment", assignmentPath+".state", fmt.Sprintf("state %q is assigned more than once", assignment.State), node.ID, "")
		}
		seen[assignment.State] = true
		valueSchema := v.validateValueExpr(assignmentPath+".value", assignment.Value, node, referenceContext{}, "")
		if exists && !valueExprAssignable(assignment.Value, valueSchema, stateSchema) {
			v.error("set_variable.type_mismatch", assignmentPath+".value", fmt.Sprintf("%s cannot be assigned to %s", describeSchema(valueSchema), describeSchema(stateSchema)), node.ID, "")
		}
	}
}

func (v *validator) validateSwitch(path string, node *Node) {
	if !authoringMapContains(compilerAuthoringCapabilities.SwitchModes, node.Switch.Mode) {
		v.error("switch.invalid_mode", path+".switch.mode", "mode must be first_match or all_match", node.ID, "")
	}
	if len(node.Switch.Cases) < 2 {
		v.error("switch.cases_required", path+".switch.cases", "switch requires a condition and one default case", node.ID, "")
	}
	ports, defaults := map[string]bool{}, 0
	for i, switchCase := range node.Switch.Cases {
		casePath := fmt.Sprintf("%s.switch.cases[%d]", path, i)
		v.validatePort(casePath+".port", switchCase.Port, node.ID)
		if ports[switchCase.Port] {
			v.error("switch.duplicate_port", casePath+".port", fmt.Sprintf("duplicate port %q", switchCase.Port), node.ID, "")
		}
		ports[switchCase.Port] = true
		if switchCase.IsDefault {
			defaults++
			if i != len(node.Switch.Cases)-1 {
				v.error("switch.default_not_last", casePath, "default case must be the final switch case", node.ID, "")
			}
			if switchCase.When != nil {
				v.error("switch.default_condition", casePath+".when", "default case cannot have a condition", node.ID, "")
			}
		} else if switchCase.When == nil {
			v.error("switch.condition_required", casePath+".when", "non-default case requires when", node.ID, "")
		} else {
			v.validatePredicate(casePath+".when", *switchCase.When, node, nil)
		}
	}
	if defaults != 1 {
		v.error("switch.default_count", path+".switch.cases", "switch requires exactly one default case", node.ID, "")
	}
}

func (v *validator) validateFork(path string, node *Node) {
	if len(node.Fork.Branches) < 2 {
		v.error("fork.branches_required", path+".fork.branches", "fork requires at least two branches", node.ID, "")
	}
	seen := map[string]bool{}
	for i, branch := range node.Fork.Branches {
		branchPath := fmt.Sprintf("%s.fork.branches[%d]", path, i)
		v.validatePort(branchPath, branch, node.ID)
		if seen[branch] {
			v.error("fork.duplicate_branch", branchPath, fmt.Sprintf("duplicate branch %q", branch), node.ID, "")
		}
		seen[branch] = true
	}
}

func (v *validator) validateLoop(path string, node *Node) {
	loop := node.Loop
	if !authoringMapContains(compilerAuthoringCapabilities.LoopModes, loop.Mode) {
		v.error("loop.invalid_mode", path+".loop.mode", "mode must be counted", node.ID, "")
	}
	if loop.Iterations < 1 {
		v.error("loop.invalid_iterations", path+".loop.iterations", "iterations must be at least 1", node.ID, "")
	} else if loop.Iterations > MaxLoopIterations {
		v.error("loop.iterations_too_large", path+".loop.iterations", fmt.Sprintf("iterations must not exceed %d", MaxLoopIterations), node.ID, "")
	}
	if len(node.Loop.State) == 0 {
		v.error("loop.state_required", path+".loop.state", "loop state contract is required", node.ID, "")
	}
	v.validateSchemaMap(path+".loop.state", node.Loop.State, node.ID)
	v.rejectAuthoredWorkItemsSchemas(path+".loop.state", node.Loop.State, node.ID)
}

func (v *validator) validatePort(path, port, nodeID string) {
	if !idPattern.MatchString(port) {
		v.error("graph.invalid_port", path, "port must match "+idPattern.String(), nodeID, "")
	}
}

func (v *validator) validateDuration(path, raw, nodeID string) {
	duration, err := parsePlanDuration(raw)
	if err != nil || duration <= 0 {
		v.error("node.invalid_duration", path, "duration must be a positive Go or ISO-8601 duration", nodeID, "")
	}
}

func (v *validator) validateRetry(path string, retry RetryPolicy, nodeID string) {
	if retry.MaxAttempts < 1 {
		v.error("retry.invalid_attempts", path+".maxAttempts", "maxAttempts must be at least 1", nodeID, "")
	} else if retry.MaxAttempts > MaxRetryAttempts {
		v.error("retry.too_many_attempts", path+".maxAttempts", fmt.Sprintf("maxAttempts must not exceed %d", MaxRetryAttempts), nodeID, "")
	}
	v.validateDuration(path+".backoff", retry.Backoff, nodeID)
}

func parsePlanDuration(raw string) (time.Duration, error) {
	value := strings.TrimSpace(raw)
	if value == "" {
		return 0, fmt.Errorf("duration is required")
	}
	if !strings.HasPrefix(value, "PT") {
		return time.ParseDuration(value)
	}
	rest, total := strings.TrimPrefix(value, "PT"), time.Duration(0)
	lastUnitOrder := 0
	for rest != "" {
		i := 0
		for i < len(rest) && rest[i] >= '0' && rest[i] <= '9' {
			i++
		}
		if i == 0 || i == len(rest) {
			return 0, fmt.Errorf("invalid ISO duration")
		}
		number, err := strconv.ParseInt(rest[:i], 10, 64)
		if err != nil {
			return 0, err
		}
		var (
			unit      time.Duration
			unitOrder int
		)
		switch rest[i] {
		case 'H':
			unit, unitOrder = time.Hour, 1
		case 'M':
			unit, unitOrder = time.Minute, 2
		case 'S':
			unit, unitOrder = time.Second, 3
		default:
			return 0, fmt.Errorf("invalid ISO duration unit")
		}
		if unitOrder <= lastUnitOrder {
			return 0, fmt.Errorf("ISO duration units must be unique and ordered H, M, S")
		}
		lastUnitOrder = unitOrder

		unitNanos := int64(unit)
		if number > math.MaxInt64/unitNanos {
			return 0, fmt.Errorf("ISO duration exceeds maximum duration")
		}
		component := time.Duration(number * unitNanos)
		if total > time.Duration(math.MaxInt64)-component {
			return 0, fmt.Errorf("ISO duration exceeds maximum duration")
		}
		total += component
		rest = rest[i+1:]
	}
	if total <= 0 {
		return 0, fmt.Errorf("duration must be positive")
	}
	return total, nil
}

func (v *validator) validateTemplate(path, value string, node *Node) {
	if len(value) > MaxTemplateBytes {
		v.error("resource.template_too_large", path, fmt.Sprintf("template must not exceed %d bytes", MaxTemplateBytes), node.ID, "")
		return
	}
	stripped := templatePattern.ReplaceAllString(value, "")
	if strings.Contains(stripped, "{{") || strings.Contains(stripped, "}}") {
		v.error("template.invalid", path, "template contains an unmatched delimiter", node.ID, "")
	}
	for _, match := range templatePattern.FindAllStringSubmatch(value, -1) {
		expression := strings.TrimSpace(match[1])
		v.validateExpression(path, expression, node, referenceContext{}, false, true)
	}
}

func (v *validator) validateMessageTargetTemplateTypes(path, value string, node *Node) {
	for _, match := range templatePattern.FindAllStringSubmatch(value, -1) {
		expression := strings.TrimSpace(match[1])
		schema, _, typed := v.inferTypedExpression(expression, node, referenceContext{})
		if !typed {
			continue
		}
		switch {
		case planSchemaContainsScope(schema):
			v.error(
				"send_message.target_type",
				path,
				"target template cannot interpolate a scope value; use sendMessage.scope",
				node.ID,
				"",
			)
		case planSchemaContainsAgent(schema):
			v.error(
				"send_message.target_type",
				path,
				"target template cannot interpolate an agent value; use a valid message destination and select the callback Agent with sendMessage.responder",
				node.ID,
				"",
			)
		}
	}
}

func planSchemaContainsScope(schema Schema) bool {
	return schema.Type == TypeScope ||
		schema.Type == TypeArray && schema.Items != nil && planSchemaContainsScope(*schema.Items)
}

func planSchemaContainsAgent(schema Schema) bool {
	return schema.Type == TypeAgent || schema.Type == TypeResponder ||
		schema.Type == TypeArray && schema.Items != nil && planSchemaContainsAgent(*schema.Items)
}

func (v *validator) validatePredicate(path string, predicate Predicate, node *Node, loop *AgentTeamConfig) {
	v.validateExpression(path, string(predicate), node, referenceContext{loop: loop}, true, false)
}

func (v *validator) validateValueExpr(path string, expression ValueExpr, node *Node, references referenceContext, edgeID string) Schema {
	nodeID := ""
	if node != nil {
		nodeID = node.ID
	}
	kinds := compilerAuthoringCapabilities.presentValueExpressionKinds(expression)
	if len(kinds) != 1 {
		v.error("expression.one_of", path, "exactly one of ref, value, or expr is required", nodeID, edgeID)
		return Schema{}
	}
	switch kinds[0] {
	case valueExpressionRef:
		schema, ok := v.referenceSchema(expression.Ref, node, references)
		if !ok {
			v.error("expression.unknown_reference", path+".ref", fmt.Sprintf("reference %q is not declared", expression.Ref), nodeID, edgeID)
			return Schema{}
		}
		return schema
	case valueExpressionExpr:
		return v.validateExpression(path+".expr", expression.Expr, node, references, false, false)
	case valueExpressionLiteral:
		if !v.validateLiteral(path+".value", *expression.Value, nodeID, edgeID) {
			return Schema{}
		}
		if _, supported := compilerAuthoringCapabilities.valueLiteralKind(*expression.Value); !supported {
			v.error("expression.unsupported_literal", path+".value", "literal is not a supported Plan value", nodeID, edgeID)
			return Schema{}
		}
		literalSchema := inferredSchema(*expression.Value)
		if literalSchema.Type == "" {
			v.error("expression.unsupported_literal", path+".value", "literal is not a supported Plan value", nodeID, edgeID)
		}
		return literalSchema
	default:
		panic("unsupported compiler authoring value expression kind")
	}
}

type referenceContext struct {
	loop                        *AgentTeamConfig
	sourcePort                  string
	joinMerge                   bool
	activationOutputUnavailable bool
}

func (v *validator) referenceSchema(reference string, node *Node, context referenceContext) (Schema, bool) {
	segments := strings.Split(reference, ".")
	if len(segments) == 2 && segments[0] == "item" && node != nil && node.Type == NodeFanOut && node.FanOut != nil && context.sourcePort == "item" {
		schema, ok := FanOutItemContract()[segments[1]]
		return schema, ok
	}
	if context.joinMerge {
		if schema, ok := v.joinReferenceSchema(segments, node); ok {
			return schema, true
		}
	}
	if len(segments) != 2 && !(len(segments) == 3 && segments[0] == "plan" && segments[1] == "input") && !(len(segments) == 3 && segments[0] == "loop") {
		return Schema{}, false
	}
	var root map[string]Schema
	key := segments[len(segments)-1]
	switch segments[0] {
	case "input":
		root = node.Input
	case "output":
		if context.joinMerge || context.activationOutputUnavailable {
			return Schema{}, false
		}
		if context.sourcePort != "" && !nodeOutputAvailable(node, context.sourcePort) {
			return Schema{}, false
		}
		root = nodeOutput(node)
	case "state":
		root = v.document.State
	case "plan":
		root = v.document.Input
	case "loop":
		if context.loop == nil {
			return Schema{}, false
		}
		if len(segments) == 2 {
			if !context.loop.counted() || context.sourcePort != "body" {
				return Schema{}, false
			}
			switch segments[1] {
			case "iteration":
				return guaranteedSchema(Schema{Type: TypeNumber}), true
			case "isLast":
				return guaranteedSchema(Schema{Type: TypeBoolean}), true
			default:
				return Schema{}, false
			}
		}
		switch segments[1] {
		case "state":
			if context.sourcePort != "" && context.sourcePort != "body" && context.sourcePort != "done" {
				return Schema{}, false
			}
			root = context.loop.State
		default:
			return Schema{}, false
		}
	default:
		return Schema{}, false
	}
	schema, ok := root[key]
	return schema, ok
}

func nodeOutputAvailable(node *Node, sourcePort string) bool {
	if node == nil || sourcePort != "completed" {
		return false
	}
	switch node.Type {
	case NodeSendMessage:
		return node.SendMessage != nil && (node.SendMessage.Completion == CompletionCallback || node.SendMessage.StartThread)
	case NodeJoin:
		return node.Join != nil
	default:
		return false
	}
}

func (v *validator) joinReferenceSchema(segments []string, node *Node) (Schema, bool) {
	if node == nil || node.Type != NodeJoin || node.Join == nil {
		return Schema{}, false
	}
	if node.Join.FanOutID != "" {
		if len(segments) == 1 && segments[0] == "items" {
			return guaranteedSchema(Schema{Type: TypeArray, Items: &Schema{Type: TypeObject}}), true
		}
		return Schema{}, false
	}
	branches, ownerOK := v.joinOwnerBranches(node.Join.ForkID)
	switch node.Join.Mode {
	case JoinAll:
		if len(segments) == 1 && segments[0] == "branches" {
			return guaranteedSchema(Schema{Type: TypeObject}), true
		}
		if len(segments) < 2 || len(segments) > 3 || segments[0] != "branches" || !ownerOK || !contains(branches, segments[1]) {
			return Schema{}, false
		}
		if len(segments) == 2 {
			return guaranteedSchema(Schema{Type: TypeObject}), true
		}
		schema, ok := node.Input[segments[2]]
		return schema, ok
	case JoinAny:
		if len(segments) == 2 && segments[0] == "winner" && segments[1] == "branch" && ownerOK {
			return guaranteedSchema(Schema{Type: TypeEnum, Values: branches}), true
		}
		if len(segments) >= 2 && len(segments) <= 3 && segments[0] == "winner" && segments[1] == "value" {
			if len(segments) == 2 {
				return guaranteedSchema(Schema{Type: TypeObject}), true
			}
			schema, ok := node.Input[segments[2]]
			return schema, ok
		}
	}
	return Schema{}, false
}

func nodeOutput(node *Node) map[string]Schema {
	if node == nil {
		return nil
	}
	switch node.Type {
	case NodeSendMessage:
		if node.SendMessage != nil {
			return sendMessageCompletedOutput(node.SendMessage)
		}
	case NodeJoin:
		if node.Join != nil {
			return node.Join.Result
		}
	case NodeLoop:
		if node.Loop != nil {
			return node.Loop.outputContract()
		}
	}
	return nil
}
