package run

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"unicode/utf8"

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

const (
	workItemInstructionReferenceFailureCode = "work_item.instruction_reference_invalid"

	maxWorkItemInstructionReferences       = 64
	maxWorkItemInstructionPathSegments     = 8
	maxWorkItemInstructionPathSegmentBytes = 64
	maxWorkItemInstructionScalarBytes      = 8 << 10
	// Keep dynamic materialization below the compiler's ordinary template
	// ceiling and independently bounded even when one scalar is referenced
	// repeatedly.
	maxMaterializedWorkItemInstructionBytes = 32 << 10
)

// workItemInstructionToken records either an escaped dollar ("$$") or one
// dependency output reference. Offsets are byte offsets into the immutable
// WorkItem instruction. Keeping offsets instead of rewritten fragments makes
// expansion single-pass: a '$' supplied by a dependency value is never parsed
// as another reference.
type workItemInstructionToken struct {
	start         int
	end           int
	literalDollar bool
	dependencyKey string
	valuePath     []string
}

// validateNormalizedWorkItemKeyAt validates the WorkItem key pattern and attaches
// the item index to the structured diagnostic when known.
func validateNormalizedWorkItemKeyAt(path, key string, index int) error {
	code := validationCodeWorkItemsKeyInvalid
	var indexPtr *int
	if index >= 0 {
		value := index
		indexPtr = &value
	}
	if len(key) == 0 || len(key) > plan.MaxWorkItemKeyBytes || key[0] < 'a' || key[0] > 'z' {
		return validationErrorf(code, path, plan.WorkItemFieldKey, indexPtr,
			"%s must match ^[a-z][a-z0-9_-]{0,255}$", path)
	}
	for i := 1; i < len(key); i++ {
		value := key[i]
		if value >= 'a' && value <= 'z' || value >= '0' && value <= '9' || value == '_' || value == '-' {
			continue
		}
		return validationErrorf(code, path, plan.WorkItemFieldKey, indexPtr,
			"%s must match ^[a-z][a-z0-9_-]{0,255}$", path)
	}
	return nil
}

// validateWorkItemInstructionReferences is the callback-boundary validation
// half of instruction materialization. It deliberately knows only the direct
// dependsOn keys: an indirect or unknown key cannot become valid later merely
// because it exists elsewhere in the WorkItem DAG.
func validateWorkItemInstructionReferences(
	path string,
	instruction string,
	directDependencies []string,
) error {
	_, err := parseWorkItemInstruction(path, instruction, directDependencies)
	return err
}

// ValidateWorkItemInstructionDependencyContract applies the statically
// decidable half of dependency-reference validation at the trusted Planner
// callback boundary. dependencyValueContract is the frozen contract for every
// dependencies.<key>.value object in this fan-out.
//
// Artifact object schemas intentionally do not describe their internal
// properties. Consequently this validator proves only that a reference starts
// at a declared top-level field and that the declared type can lead to a
// materializable scalar. The runtime resolver remains authoritative for paths
// below an object and for the presence of optional fields.
func ValidateWorkItemInstructionDependencyContract(
	path string,
	items []WorkItem,
	dependencyValueContract plan.ArtifactContract,
) error {
	topLevel := make(map[string]plan.ArtifactSchema, len(dependencyValueContract))
	ambiguous := make(map[string]struct{})
	for _, field := range dependencyValueContract {
		if _, exists := topLevel[field.Name]; exists {
			ambiguous[field.Name] = struct{}{}
			continue
		}
		topLevel[field.Name] = field.Schema
	}

	for index, item := range items {
		instructionPath := fmt.Sprintf("%s[%d].%s", path, index, plan.WorkItemFieldInstruction)
		tokens, err := parseWorkItemInstruction(instructionPath, item.Instruction, item.DependsOn)
		if err != nil {
			return err
		}
		for _, token := range tokens {
			if token.literalDollar {
				continue
			}
			topLevelName := token.valuePath[0]
			reference := item.Instruction[token.start:token.end]
			if _, duplicate := ambiguous[topLevelName]; duplicate {
				return validationErrorf(validationCodeInstructionContractAmbiguous, instructionPath, plan.WorkItemFieldInstruction, intPtr(index),
					"%s reference %q is ambiguous: dependency value contract declares top-level field %q more than once",
					instructionPath,
					reference,
					topLevelName,
				)
			}
			schema, declared := topLevel[topLevelName]
			if !declared {
				return validationErrorf(validationCodeInstructionContractUndeclared, instructionPath, plan.WorkItemFieldInstruction, intPtr(index),
					"%s reference %q is invalid: dependency value contract has no top-level field %q",
					instructionPath,
					reference,
					topLevelName,
				)
			}
			if err := validateWorkItemInstructionReferenceSchema(instructionPath, index, reference, schema, token.valuePath); err != nil {
				return err
			}
		}
	}
	return nil
}

// validateWorkItemInstructionReferenceSchema validates the statically decidable
// reference shape against the dependency value contract. The caller supplies the
// full instruction path and item index so the structured diagnostic carries the
// concrete path/field/index.
func validateWorkItemInstructionReferenceSchema(
	instructionPath string,
	index int,
	reference string,
	schema plan.ArtifactSchema,
	valuePath []string,
) error {
	topLevelName := valuePath[0]
	indexPtr := intPtr(index)
	switch schema.Type {
	case plan.TypeNumber, plan.TypeBoolean:
		if len(valuePath) != 1 {
			return validationErrorf(validationCodeInstructionContractType, instructionPath, plan.WorkItemFieldInstruction, indexPtr,
				"%s reference %q is invalid: top-level field %q is %s and cannot be traversed; number and boolean references must use exactly one path segment",
				instructionPath,
				reference,
				topLevelName,
				schema.Type,
			)
		}
		return nil
	case plan.TypeObject:
		if len(valuePath) == 1 {
			return validationErrorf(validationCodeInstructionContractObjectLeaf, instructionPath, plan.WorkItemFieldInstruction, indexPtr,
				"%s reference %q is invalid: top-level field %q is an object and cannot be materialized directly; continue to an internal field",
				instructionPath,
				reference,
				topLevelName,
			)
		}
		// ArtifactSchema does not encode object properties. The runtime resolver
		// validates the remaining segments and accepts only number, boolean, or
		// null once the dependency has produced its actual value.
		return nil
	case plan.TypeString,
		plan.TypeEnum,
		plan.TypeMember,
		plan.TypeAgent,
		plan.TypeResponder,
		plan.TypeChannel,
		plan.TypeScope,
		plan.TypeDuration,
		plan.TypeDatetime,
		plan.TypeArray,
		plan.TypeWorkItems:
		if len(valuePath) != 1 {
			return validationErrorf(validationCodeInstructionContractType, instructionPath, plan.WorkItemFieldInstruction, indexPtr,
				"%s reference %q is invalid: top-level field %q is %s and cannot be traversed",
				instructionPath,
				reference,
				topLevelName,
				schema.Type,
			)
		}
		return validationErrorf(validationCodeInstructionContractType, instructionPath, plan.WorkItemFieldInstruction, indexPtr,
			"%s reference %q is invalid: top-level field %q has non-materializable type %s; references must resolve to number, boolean, or an object-internal runtime scalar",
			instructionPath,
			reference,
			topLevelName,
			schema.Type,
		)
	default:
		return validationErrorf(validationCodeInstructionContractType, instructionPath, plan.WorkItemFieldInstruction, indexPtr,
			"%s reference %q is invalid: top-level field %q has unsupported type %q",
			instructionPath,
			reference,
			topLevelName,
			schema.Type,
		)
	}
}

// materializeWorkItemInstruction replaces direct-dependency output references
// with scalar text after those dependencies have succeeded. The structured
// dependency envelopes remain unchanged and are still supplied separately as
// item.dependencies.
func materializeWorkItemInstruction(
	path string,
	instruction string,
	directDependencies []string,
	dependencies Values,
) (string, error) {
	tokens, err := parseWorkItemInstruction(path, instruction, directDependencies)
	if err != nil {
		return "", err
	}

	var builder strings.Builder
	initialCapacity := len(instruction)
	if initialCapacity > maxMaterializedWorkItemInstructionBytes {
		initialCapacity = maxMaterializedWorkItemInstructionBytes
	}
	builder.Grow(initialCapacity)
	cursor := 0
	for _, token := range tokens {
		if err := appendBoundedWorkItemInstructionText(&builder, instruction[cursor:token.start]); err != nil {
			return "", fmt.Errorf("%s: %w", path, err)
		}
		if token.literalDollar {
			if err := appendBoundedWorkItemInstructionText(&builder, "$"); err != nil {
				return "", fmt.Errorf("%s: %w", path, err)
			}
		} else {
			value, resolveErr := resolveWorkItemInstructionReference(
				path,
				token,
				dependencies,
			)
			if resolveErr != nil {
				return "", resolveErr
			}
			text, formatErr := formatWorkItemInstructionReferenceValue(value)
			if formatErr != nil {
				return "", fmt.Errorf(
					"%s reference %q: %w",
					path,
					instruction[token.start:token.end],
					formatErr,
				)
			}
			if err := appendBoundedWorkItemInstructionText(&builder, text); err != nil {
				return "", fmt.Errorf("%s: %w", path, err)
			}
		}
		cursor = token.end
	}
	if err := appendBoundedWorkItemInstructionText(&builder, instruction[cursor:]); err != nil {
		return "", fmt.Errorf("%s: %w", path, err)
	}
	return builder.String(), nil
}

func parseWorkItemInstruction(
	path string,
	instruction string,
	directDependencies []string,
) ([]workItemInstructionToken, error) {
	index := workItemIndexFromPath(path)
	if !utf8.ValidString(instruction) {
		return nil, validationErrorf(validationCodeInstructionUTF8, path, plan.WorkItemFieldInstruction, index,
			"%s must be valid UTF-8", path)
	}
	keys := append([]string(nil), directDependencies...)
	// A key may itself prefix another key. Longest match is part of the DSL so
	// the result never depends on callback array or map iteration order.
	sort.Slice(keys, func(left, right int) bool {
		if len(keys[left]) == len(keys[right]) {
			return keys[left] < keys[right]
		}
		return len(keys[left]) > len(keys[right])
	})

	tokens := make([]workItemInstructionToken, 0)
	referenceCount := 0
	// Compute the callback-time lower bound while scanning, before retaining the
	// next token. In particular, a transport-sized string of "$$" escapes must
	// stop after the 32 KiB materialized ceiling instead of allocating one token
	// for every pair and only discovering the guaranteed overflow at the end.
	// Every reference can render to a one-byte number (for example, 0), and every
	// escaped "$$" renders to one literal dollar.
	minimumMaterializedBytes := 0
	addMinimumMaterializedBytes := func(delta int) error {
		if delta > maxMaterializedWorkItemInstructionBytes-minimumMaterializedBytes {
			return validationErrorf(validationCodeInstructionMinimumExceeded, path, plan.WorkItemFieldInstruction, index,
				"%s minimum materialized instruction must not exceed %d bytes",
				path,
				maxMaterializedWorkItemInstructionBytes,
			)
		}
		minimumMaterializedBytes += delta
		return nil
	}
	for cursor := 0; cursor < len(instruction); {
		relative := strings.IndexByte(instruction[cursor:], '$')
		if relative < 0 {
			if err := addMinimumMaterializedBytes(len(instruction) - cursor); err != nil {
				return nil, err
			}
			break
		}
		start := cursor + relative
		if err := addMinimumMaterializedBytes(start - cursor); err != nil {
			return nil, err
		}
		if start+1 < len(instruction) && instruction[start+1] == '$' {
			if err := addMinimumMaterializedBytes(1); err != nil {
				return nil, err
			}
			tokens = append(tokens, workItemInstructionToken{
				start: start, end: start + 2, literalDollar: true,
			})
			cursor = start + 2
			continue
		}
		referenceCount++
		if referenceCount > maxWorkItemInstructionReferences {
			return nil, validationErrorf(validationCodeInstructionTooMany, path, plan.WorkItemFieldInstruction, index,
				"%s must not contain more than %d dependency references",
				path,
				maxWorkItemInstructionReferences,
			)
		}

		key, afterKey := matchDirectWorkItemDependency(instruction, start+1, keys)
		if key == "" {
			return nil, validationErrorf(validationCodeInstructionReference, path, plan.WorkItemFieldInstruction, index,
				"%s has invalid dependency reference at byte %d: every unescaped '$' must reference a direct dependsOn key",
				path,
				start,
			)
		}
		segments, end, parseErr := parseWorkItemValuePath(instruction, afterKey+1)
		if parseErr != nil {
			return nil, fmt.Errorf(
				"%s has invalid reference for direct dependsOn key %q at byte %d: invalid business output path: %w",
				path,
				key,
				start,
				parseErr,
			)
		}
		if err := addMinimumMaterializedBytes(1); err != nil {
			return nil, err
		}
		tokens = append(tokens, workItemInstructionToken{
			start: start, end: end, dependencyKey: key, valuePath: segments,
		})
		cursor = end
	}
	return tokens, nil
}

// matchDirectWorkItemDependency matches the already length-sorted direct keys
// and requires the key to end at a path separator. A bare "$key" is therefore
// rejected instead of being silently emitted as ordinary text.
func matchDirectWorkItemDependency(instruction string, start int, keys []string) (string, int) {
	for _, key := range keys {
		end := start + len(key)
		if key == "" || end >= len(instruction) || instruction[end] != '.' {
			continue
		}
		if strings.HasPrefix(instruction[start:], key) {
			return key, end
		}
	}
	return "", 0
}

// parseWorkItemValuePath consumes a business-value path relative to the
// dependency's callback output. For example, "$op08.result" resolves
// dependency envelope value.result; the envelope itself remains inaccessible.
func parseWorkItemValuePath(instruction string, start int) ([]string, int, error) {
	segments := make([]string, 0, 3)
	cursor := start
	for {
		if cursor >= len(instruction) || !isWorkItemPathSegmentStart(instruction[cursor]) {
			return nil, 0, fmt.Errorf("path segments must match [A-Za-z_][A-Za-z0-9_-]*")
		}
		segmentStart := cursor
		cursor++
		for cursor < len(instruction) && isWorkItemPathSegmentContinue(instruction[cursor]) {
			cursor++
		}
		segments = append(segments, instruction[segmentStart:cursor])
		if len(segments[len(segments)-1]) > maxWorkItemInstructionPathSegmentBytes {
			return nil, 0, validationErrorf(validationCodeInstructionSegment, "", "", nil,
				"path segment must not exceed %d bytes",
				maxWorkItemInstructionPathSegmentBytes,
			)
		}
		if len(segments) > maxWorkItemInstructionPathSegments {
			return nil, 0, validationErrorf(validationCodeInstructionTooMany, "", "", nil,
				"path must not contain more than %d segments",
				maxWorkItemInstructionPathSegments,
			)
		}
		switch segments[len(segments)-1] {
		case "__proto__", "prototype", "constructor":
			return nil, 0, validationErrorf(validationCodeInstructionForbidden, "", "", nil,
				"path segment %q is forbidden", segments[len(segments)-1])
		}
		if cursor >= len(instruction) || instruction[cursor] != '.' {
			break
		}
		if cursor+1 < len(instruction) && isWorkItemPathSegmentStart(instruction[cursor+1]) {
			cursor++
			continue
		}
		if cursor+1 < len(instruction) &&
			(instruction[cursor+1] == '[' || instruction[cursor+1] == ']') {
			return nil, 0, validationErrorf(validationCodeInstructionPath, "", "", nil,
				"array or bracket path syntax is not supported")
		}
		// A final full stop is ordinary prose. Characters that look like an
		// attempted but illegal next segment fail closed instead of producing
		// a partially resolved reference followed by misleading path text.
		if cursor+1 < len(instruction) &&
			(isWorkItemPathSegmentContinue(instruction[cursor+1]) || instruction[cursor+1] == '.') {
			return nil, 0, fmt.Errorf("path segments must match [A-Za-z_][A-Za-z0-9_-]*")
		}
		break
	}
	if cursor < len(instruction) && (instruction[cursor] == '[' || instruction[cursor] == ']') {
		return nil, 0, validationErrorf(validationCodeInstructionPath, "", "", nil,
			"array or bracket path syntax is not supported")
	}
	return segments, cursor, nil
}

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

// workItemIndexFromPath extracts the array index embedded in a callback path of
// the form callback.workItems[N].instruction. It returns nil when the path has
// no [N] segment so the structured diagnostic omits the index.
func workItemIndexFromPath(path string) *int {
	start := strings.LastIndexByte(path, '[')
	if start < 0 {
		return nil
	}
	end := strings.IndexByte(path[start+1:], ']')
	if end < 0 {
		return nil
	}
	value := 0
	digits := path[start+1 : start+1+end]
	if digits == "" {
		return nil
	}
	for _, digit := range digits {
		if digit < '0' || digit > '9' {
			return nil
		}
		value = value*10 + int(digit-'0')
	}
	return &value
}

func isWorkItemPathSegmentContinue(value byte) bool {
	return isWorkItemPathSegmentStart(value) || value >= '0' && value <= '9' || value == '-'
}

func resolveWorkItemInstructionReference(
	path string,
	token workItemInstructionToken,
	dependencies Values,
) (any, error) {
	rawEnvelope, found := dependencies[token.dependencyKey]
	if !found {
		return nil, validationErrorf(validationCodeDependencyMissing, path, plan.WorkItemFieldInstruction, nil,
			"%s dependency %q has no frozen result", path, token.dependencyKey)
	}
	envelope, ok := workItemInstructionObject(rawEnvelope)
	if !ok {
		return nil, validationErrorf(validationCodeDependencyOutput, path, plan.WorkItemFieldInstruction, nil,
			"%s dependency %q result envelope is %T, want object", path, token.dependencyKey, rawEnvelope)
	}
	status, ok := envelope["status"].(string)
	if !ok || status != string(WorkItemSucceeded) {
		return nil, validationErrorf(validationCodeDependencyNotSucceeded, path, plan.WorkItemFieldInstruction, nil,
			"%s dependency %q has not succeeded", path, token.dependencyKey)
	}
	current, found := envelope["value"]
	if !found {
		return nil, validationErrorf(validationCodeDependencyOutput, path, plan.WorkItemFieldInstruction, nil,
			"%s dependency %q has no output", path, token.dependencyKey)
	}
	for _, segment := range token.valuePath {
		object, objectOK := workItemInstructionObject(current)
		if !objectOK {
			return nil, validationErrorf(validationCodeDependencyTraversal, path, plan.WorkItemFieldInstruction, nil,
				"%s dependency %q output path %q traverses non-object %T",
				path,
				token.dependencyKey,
				strings.Join(token.valuePath, "."),
				current,
			)
		}
		current, found = object[segment]
		if !found {
			return nil, validationErrorf(validationCodeDependencyTraversal, path, plan.WorkItemFieldInstruction, nil,
				"%s dependency %q output path %q does not exist",
				path,
				token.dependencyKey,
				strings.Join(token.valuePath, "."),
			)
		}
	}
	return current, nil
}

func workItemInstructionObject(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 formatWorkItemInstructionReferenceValue(value any) (string, error) {
	var text string
	switch typed := value.(type) {
	case json.Number:
		number, err := normalizeNumber(typed)
		if err != nil {
			return "", fmt.Errorf("resolved number is invalid: %w", err)
		}
		text = number.String()
	case bool:
		if typed {
			text = "true"
		} else {
			text = "false"
		}
	case nil:
		text = "null"
	default:
		return "", validationErrorf(validationCodeInstructionScalarExceeded, "", "", nil,
			"resolved value is %T, want number, boolean, or null", value)
	}
	if len(text) > maxWorkItemInstructionScalarBytes {
		return "", validationErrorf(validationCodeInstructionScalarExceeded, "", "", nil,
			"resolved scalar must not exceed %d bytes",
			maxWorkItemInstructionScalarBytes,
		)
	}
	return text, nil
}

func appendBoundedWorkItemInstructionText(builder *strings.Builder, text string) error {
	if len(text) > maxMaterializedWorkItemInstructionBytes-builder.Len() {
		return validationErrorf(validationCodeInstructionMaterializedExceeded, "", "", nil,
			"materialized instruction must not exceed %d bytes",
			maxMaterializedWorkItemInstructionBytes,
		)
	}
	builder.WriteString(text)
	return nil
}
