package plan

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"sort"
	"strings"
	"time"

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

// InputError is a stable, path-aware failure returned while normalizing JSON
// against a compiled Plan input contract. Callers may use Code for product
// errors without parsing the human-readable message.
type InputError struct {
	Code    string
	Path    string
	Message string
}

func (err InputError) Error() string {
	if err.Path == "" {
		return err.Message
	}
	return err.Path + ": " + err.Message
}

// NormalizedInput is one immutable, normalized partial or final Plan input
// object. Its values are kept private so FinalizeInput can safely layer values
// without accepting an unvalidated map assembled by another package.
type NormalizedInput struct {
	values map[string]json.RawMessage
}

// DeterministicJSON returns a deterministic JSON object. Object keys are emitted
// in lexical order by encoding/json and every value was normalized when this
// value was constructed.
func (input NormalizedInput) DeterministicJSON() ([]byte, error) {
	values := input.values
	if values == nil {
		values = map[string]json.RawMessage{}
	}
	payload, err := json.Marshal(values)
	if err != nil {
		return nil, fmt.Errorf("encode normalized Plan input: %w", err)
	}
	// A NormalizedInput can be assembled from several independently legal
	// layers. Recheck the aggregate here so layering cannot bypass the same
	// byte/depth/value budget enforced for one caller-authored object.
	if _, err := decodeStrictInputObject(payload); err != nil {
		return nil, err
	}
	return payload, nil
}

// Keys returns the normalized field names in lexical order.
func (input NormalizedInput) Keys() []string {
	keys := make([]string, 0, len(input.values))
	for key := range input.values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

// Has reports whether the normalized object contains one field.
func (input NormalizedInput) Has(name string) bool {
	_, ok := input.values[name]
	return ok
}

// ValueJSON returns a defensive copy of one deterministic field value.
func (input NormalizedInput) ValueJSON(name string) (json.RawMessage, bool) {
	value, ok := input.values[name]
	if !ok {
		return nil, false
	}
	return append(json.RawMessage(nil), value...), true
}

// NormalizePartialInputJSON strictly parses one JSON object, rejects duplicate
// and unknown fields, validates every supplied value against the compiled Plan
// input contract, and returns normalized runtime values. It does not insert Plan
// defaults and does not require omitted required fields.
func NormalizePartialInputJSON(contract ArtifactContract, raw []byte) (NormalizedInput, error) {
	decoded, err := decodeStrictInputObject(raw)
	if err != nil {
		return NormalizedInput{}, err
	}
	fields, err := artifactContractIndex(contract)
	if err != nil {
		return NormalizedInput{}, err
	}
	result := NormalizedInput{values: make(map[string]json.RawMessage, len(decoded))}
	for name, value := range decoded {
		schema, ok := fields[name]
		if !ok {
			return NormalizedInput{}, InputError{
				Code: "input.unknown_field", Path: inputPath(name),
				Message: fmt.Sprintf("field %q is not declared by the Plan input contract", name),
			}
		}
		normalized, err := normalizeInputValue(value, schema, inputPath(name))
		if err != nil {
			return NormalizedInput{}, err
		}
		payload, err := json.Marshal(normalized)
		if err != nil {
			return NormalizedInput{}, fmt.Errorf("encode normalized Plan input field %q: %w", name, err)
		}
		result.values[name] = payload
	}
	return result, nil
}

// NormalizeFinalInputJSON performs partial normalization, applies Plan defaults,
// and rejects any remaining required field. It is the Invocation boundary for
// a single, unlayered JSON input object.
func NormalizeFinalInputJSON(contract ArtifactContract, raw []byte) (NormalizedInput, error) {
	partial, err := NormalizePartialInputJSON(contract, raw)
	if err != nil {
		return NormalizedInput{}, err
	}
	return FinalizeInput(contract, partial)
}

// MergeInputs overlays already-normalized inputs from left to right without
// adding Plan defaults or enforcing required fields.
func MergeInputs(layers ...NormalizedInput) NormalizedInput {
	merged := NormalizedInput{values: map[string]json.RawMessage{}}
	for _, layer := range layers {
		for name, value := range layer.values {
			merged.values[name] = append(json.RawMessage(nil), value...)
		}
	}
	return merged
}

// FinalizeInput applies the input precedence "Plan defaults, then layers
// from left to right" and rejects missing required inputs. Binding uses this to
// implement Plan defaults < Binding defaults < occurrence values < constants.
func FinalizeInput(contract ArtifactContract, layers ...NormalizedInput) (NormalizedInput, error) {
	if _, err := artifactContractIndex(contract); err != nil {
		return NormalizedInput{}, err
	}
	result := NormalizedInput{values: map[string]json.RawMessage{}}
	for _, field := range contract {
		if field.Schema.Default != nil {
			result.values[field.Name] = append(json.RawMessage(nil), (*field.Schema.Default)...)
		}
	}
	result = MergeInputs(append([]NormalizedInput{result}, layers...)...)
	missing := MissingRequiredInputs(contract, layers...)
	if len(missing) != 0 {
		return NormalizedInput{}, InputError{
			Code: "input.required", Path: inputPath(missing[0]),
			Message: fmt.Sprintf("required Plan input %q is missing", missing[0]),
		}
	}
	if _, err := result.DeterministicJSON(); err != nil {
		return NormalizedInput{}, err
	}
	return result, nil
}

// MissingRequiredInputs reports unresolved required fields after Plan defaults
// and the supplied normalized layers are applied. The result is sorted.
func MissingRequiredInputs(contract ArtifactContract, layers ...NormalizedInput) []string {
	present := make(map[string]bool, len(contract))
	for _, field := range contract {
		if field.Schema.Default != nil {
			present[field.Name] = true
		}
	}
	for _, layer := range layers {
		for name := range layer.values {
			present[name] = true
		}
	}
	missing := make([]string, 0)
	for _, field := range contract {
		if field.Schema.Required && !present[field.Name] {
			missing = append(missing, field.Name)
		}
	}
	sort.Strings(missing)
	return missing
}

func artifactContractIndex(contract ArtifactContract) (map[string]ArtifactSchema, error) {
	fields := make(map[string]ArtifactSchema, len(contract))
	last := ""
	for index, field := range contract {
		if strings.TrimSpace(field.Name) == "" {
			return nil, fmt.Errorf("Plan input contract contains an empty field name")
		}
		if index > 0 && field.Name <= last {
			return nil, fmt.Errorf("Plan input contract fields must have unique names in ascending order")
		}
		last = field.Name
		fields[field.Name] = field.Schema
	}
	return fields, nil
}

func normalizeInputValue(value any, schema ArtifactSchema, path string) (any, error) {
	typeError := func(expected string) error {
		return InputError{Code: "input.type", Path: path, Message: "value must be " + expected}
	}
	switch schema.Type {
	case TypeChannel, TypeString, TypeMember, TypeAgent:
		text, ok := value.(string)
		if !ok {
			return nil, typeError("a string")
		}
		return text, nil
	case TypeResponder:
		return normalizeResponderInputValue(value, typeError)
	case TypeScope:
		scope, err := domain.NormalizeMessageScopeValue(value)
		if err != nil {
			return nil, InputError{Code: "input.scope", Path: path, Message: err.Error()}
		}
		return map[string]any{
			"kind":    string(scope.Kind),
			"scopeId": scope.ScopeID,
		}, nil
	case TypeEnum:
		text, ok := value.(string)
		if !ok {
			return nil, typeError("a declared enum string")
		}
		for _, allowed := range schema.Values {
			if text == allowed {
				return text, nil
			}
		}
		return nil, InputError{Code: "input.enum", Path: path, Message: fmt.Sprintf("value %q is not an allowed enum value", text)}
	case TypeNumber:
		number, ok := value.(json.Number)
		if !ok {
			return nil, typeError("a finite JSON number")
		}

		deterministic, err := normalizedArtifactNumber(string(number))
		if err != nil {
			return nil, InputError{Code: "input.number", Path: path, Message: "value must be a finite JSON number"}
		}
		return deterministic, nil
	case TypeBoolean:
		boolean, ok := value.(bool)
		if !ok {
			return nil, typeError("a boolean")
		}
		return boolean, nil
	case TypeDuration:
		text, ok := value.(string)
		if !ok {
			return nil, typeError("a positive duration string")
		}
		duration, err := parsePlanDuration(text)
		if err != nil || duration <= 0 {
			return nil, InputError{Code: "input.duration", Path: path, Message: "value must be a positive duration"}
		}
		return artifactNormalizedNumber(fmt.Sprintf("%d", int64(duration))), nil
	case TypeDatetime:
		text, ok := value.(string)
		if !ok {
			return nil, typeError("an RFC3339 datetime string")
		}
		instant, err := time.Parse(time.RFC3339, text)
		if err != nil {
			return nil, InputError{Code: "input.datetime", Path: path, Message: "value must be an RFC3339 datetime"}
		}
		return instant.UTC().Format(time.RFC3339Nano), nil
	case TypeObject:
		object, ok := value.(map[string]any)
		if !ok {
			return nil, typeError("an object")
		}
		normalized, err := normalizeArtifactLiteral(object)
		if err != nil {
			return nil, InputError{Code: "input.object", Path: path, Message: err.Error()}
		}
		return normalized, nil
	case TypeArray:
		values, ok := value.([]any)
		if !ok || schema.Items == nil {
			return nil, typeError("an array")
		}
		normalized := make([]any, len(values))
		for index, item := range values {
			value, err := normalizeInputValue(item, *schema.Items, fmt.Sprintf("%s[%d]", path, index))
			if err != nil {
				return nil, err
			}
			normalized[index] = value
		}
		return normalized, nil
	default:
		return nil, InputError{Code: "input.schema", Path: path, Message: fmt.Sprintf("unsupported Plan input type %q", schema.Type)}
	}
}

func normalizeResponderInputValue(value any, typeError func(string) error) (any, error) {
	if text, ok := value.(string); ok {
		if strings.TrimSpace(text) == "" {
			return nil, typeError("a non-blank agent or selectOne pool")
		}
		return text, nil
	}
	object, ok := value.(map[string]any)
	if !ok {
		return nil, typeError("a fixed agent string or selectOne pool")
	}
	raw, present := object["selectOne"]
	if !present || len(object) != 1 {
		return nil, typeError("a selectOne candidate pool")
	}
	values, ok := raw.([]any)
	if !ok || len(values) == 0 {
		return nil, typeError("a non-empty selectOne array")
	}
	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, typeError(fmt.Sprintf("a non-blank agent at selectOne[%d]", index))
		}
		if _, duplicate := seen[candidate]; duplicate {
			continue
		}
		seen[candidate] = struct{}{}
		normalized = append(normalized, candidate)
	}
	return map[string]any{"selectOne": normalized}, nil
}

func inputPath(name string) string {
	return "$." + name
}

type strictInputJSONBudget struct {
	values int
}

func decodeStrictInputObject(raw []byte) (map[string]any, error) {
	if len(bytes.TrimSpace(raw)) == 0 {
		return nil, InputError{Code: "input.invalid_json", Path: "$", Message: "input must be one JSON object"}
	}
	if len(raw) > MaxInputBytes {
		return nil, InputError{
			Code: "input.budget", Path: "$",
			Message: fmt.Sprintf("input must not exceed %d bytes", MaxInputBytes),
		}
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	budget := &strictInputJSONBudget{}
	value, err := decodeStrictInputJSONValue(decoder, 0, budget, "$")
	if err != nil {
		return nil, err
	}
	object, ok := value.(map[string]any)
	if !ok {
		return nil, InputError{Code: "input.invalid_json", Path: "$", Message: "input must be one JSON object"}
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return nil, InputError{Code: "input.invalid_json", Path: "$", Message: "input must contain exactly one JSON value"}
		}
		return nil, InputError{Code: "input.invalid_json", Path: "$", Message: err.Error()}
	}
	return object, nil
}

func decodeStrictInputJSONValue(decoder *json.Decoder, depth int, budget *strictInputJSONBudget, path string) (any, error) {
	if depth > MaxLiteralNesting {
		return nil, InputError{Code: "input.budget", Path: path, Message: fmt.Sprintf("input nesting must not exceed %d", MaxLiteralNesting)}
	}
	budget.values++
	if budget.values > MaxLiteralValues {
		return nil, InputError{Code: "input.budget", Path: path, Message: fmt.Sprintf("input must not contain more than %d values", MaxLiteralValues)}
	}
	token, err := decoder.Token()
	if err != nil {
		return nil, InputError{Code: "input.invalid_json", Path: path, Message: err.Error()}
	}
	delimiter, isDelimiter := token.(json.Delim)
	if !isDelimiter {
		switch token.(type) {
		case nil, bool, string, json.Number:
			return token, nil
		default:
			return nil, InputError{Code: "input.invalid_json", Path: path, Message: "unsupported JSON value"}
		}
	}
	switch delimiter {
	case '{':
		object := map[string]any{}
		for decoder.More() {
			keyToken, err := decoder.Token()
			if err != nil {
				return nil, InputError{Code: "input.invalid_json", Path: path, Message: err.Error()}
			}
			key, ok := keyToken.(string)
			if !ok {
				return nil, InputError{Code: "input.invalid_json", Path: path, Message: "object key must be a string"}
			}
			childPath := inputPath(key)
			if path != "$" {
				childPath = path + "." + key
			}
			if _, exists := object[key]; exists {
				return nil, InputError{Code: "input.duplicate_field", Path: childPath, Message: fmt.Sprintf("field %q appears more than once", key)}
			}
			value, err := decodeStrictInputJSONValue(decoder, depth+1, budget, childPath)
			if err != nil {
				return nil, err
			}
			object[key] = value
		}
		if _, err := decoder.Token(); err != nil {
			return nil, InputError{Code: "input.invalid_json", Path: path, Message: err.Error()}
		}
		return object, nil
	case '[':
		array := make([]any, 0)
		for decoder.More() {
			index := len(array)
			value, err := decodeStrictInputJSONValue(decoder, depth+1, budget, fmt.Sprintf("%s[%d]", path, index))
			if err != nil {
				return nil, err
			}
			array = append(array, value)
		}
		if _, err := decoder.Token(); err != nil {
			return nil, InputError{Code: "input.invalid_json", Path: path, Message: err.Error()}
		}
		return array, nil
	default:
		return nil, InputError{Code: "input.invalid_json", Path: path, Message: "unexpected JSON delimiter"}
	}
}
