package plan

import (
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/workflow/plan/internal/expression"
)

// validateExpression checks Plan references against the context at this source
// location, then evaluates the bounded expression with representative values.
// Runtime execution does not parse this source; successful compilation lowers
// it to typed artifact instructions.
func (v *validator) validateExpression(path, source string, node *Node, references referenceContext, condition, preCompletion bool) Schema {
	if strings.TrimSpace(source) == "" {
		v.error("expression.required", path, "expression is required", node.ID, "")
		return Schema{}
	}
	if message := expressionBudgetError(source); message != "" {
		v.error("resource.expression_budget", path, message, node.ID, "")
		return Schema{}
	}
	// Preserve the complete declared schema for a direct expression reference.
	// Evaluator dummy values intentionally preserve runtime shape, not nominal
	// types or enum constraints, so relying on them would erase the contract.
	directSchema, directReference := v.referenceSchema(strings.TrimSpace(source), node, references)
	typedSchema, typedError, typed := v.inferTypedExpression(source, node, references)
	if typedError != "" {
		v.error("expression.type_mismatch", path, typedError, node.ID, "")
	}
	context := expression.Context{
		"input": {}, "output": {}, "state": {}, "plan": {}, "loop": {}, "branches": {}, "winner": {},
	}
	if !v.prepareExpressionContext(path, source, node, references, preCompletion, context) {
		return Schema{}
	}
	if condition {
		if _, err := expression.EvalCondition(source, context); err != nil {
			v.error("expression.invalid_condition", path, err.Error(), node.ID, "")
			return Schema{}
		}
		if typedError != "" {
			return Schema{}
		}
		if typed && typedSchema.Type == TypeBoolean && !schemaGuaranteed(typedSchema) {
			v.error("expression.nullable_operand", path, "condition may evaluate from an optional value", node.ID, "")
			return Schema{}
		}
		return Schema{Type: TypeBoolean}
	}
	value, err := expression.EvalValue(source, context)
	if err != nil {
		v.error("expression.invalid", path, err.Error(), node.ID, "")
		return Schema{}
	}
	if directReference {
		return directSchema
	}
	if typed && typedError == "" && typedSchema.Type != "" {
		return typedSchema
	}
	if typedError != "" {
		return Schema{}
	}
	return inferredSchema(value)
}

// prepareExpressionContext validates every reference token and installs a
// representative value under its normalized Plan root. It deliberately leaves
// the source unchanged: loop.plan/v1 has no aliases for retired Workflow
// envelope namespaces.
func (v *validator) prepareExpressionContext(path, source string, node *Node, references referenceContext, preCompletion bool, context expression.Context) bool {
	valid := true
	for i := 0; i < len(source); {
		if source[i] == '\'' || source[i] == '"' {
			quote := source[i]
			i++
			for i < len(source) {
				if source[i] == '\\' {
					i += 2
					continue
				}
				if source[i] == quote {
					i++
					break
				}
				i++
			}
			continue
		}
		if !expressionIdentStart(source[i]) {
			i++
			continue
		}
		start := i
		for i < len(source) && expressionIdentPart(source[i]) {
			i++
		}
		token := source[start:i]
		if !strings.Contains(token, ".") {
			if schema, found := v.referenceSchema(token, node, references); found {
				populateExpressionContext(context, token, schema)
			}
			continue
		}
		schema, found := v.referenceSchema(token, node, references)
		if !found {
			v.error("expression.unknown_reference", path, fmt.Sprintf("reference %q is not declared", token), node.ID, "")
			valid = false
			continue
		}
		if preCompletion && strings.HasPrefix(token, "output.") {
			v.error("template.unavailable_reference", path, fmt.Sprintf("reference %q is unavailable before completion", token), node.ID, "")
			valid = false
		}
		populateExpressionContext(context, token, schema)
	}
	return valid
}

func populateExpressionContext(context expression.Context, reference string, schema Schema) {
	root, path, dotted := strings.Cut(reference, ".")
	if !dotted {
		// branches is the only whole-root reference. Its pre-created
		// bucket is already the representative object value.
		return
	}
	bucket := context[root]
	if bucket == nil {
		bucket = map[string]any{}
		context[root] = bucket
	}
	bucket[path] = dummyValue(schema)
}

func dummyValue(schema Schema) any {
	switch schema.Type {
	case TypeNumber:
		return float64(1)
	case TypeBoolean:
		return true
	case TypeObject:
		return map[string]any{}
	case TypeArray:
		if schema.Items == nil {
			return []any{}
		}
		return []any{dummyValue(*schema.Items)}
	case TypeEnum:
		if len(schema.Values) != 0 {
			return schema.Values[0]
		}
		return ""
	default:
		return "value"
	}
}

func expressionIdentStart(value byte) bool {
	return value == '_' || value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z'
}

func expressionIdentPart(value byte) bool {
	return expressionIdentStart(value) || value >= '0' && value <= '9' || value == '.'
}
