package plan

import (
	"encoding/json"
	"fmt"
	"math"
	"sort"
)

// literalValidationIssue describes an authoring value that cannot be lowered
// into an ArtifactLiteral. Validation must find these before artifact lowering
// so malformed author input is always returned as a diagnostic, never as a Go
// error from Compile.
type literalValidationIssue struct {
	code    string
	suffix  string
	message string
}

// validateDeterministicLiteral walks an author-provided literal iteratively. The
// document-level literal budget normally rejects excessive inputs before this
// function runs, but the local guards make the invariant explicit and keep the
// helper safe for direct, already-decoded Document validation too.
//
// Only values that normalizeArtifactLiteral can encode as deterministic JSON are
// accepted. In particular, yaml.v3 may decode unquoted timestamps as time.Time,
// non-string-key objects as map[any]any, and .nan/.inf as non-finite floats;
// none of those are valid Plan literals.
func validateDeterministicLiteral(value any) *literalValidationIssue {
	type pendingLiteral struct {
		value  any
		depth  int
		suffix string
	}

	pending := []pendingLiteral{{value: value}}
	values := 0
	for len(pending) != 0 {
		last := len(pending) - 1
		current := pending[last]
		pending = pending[:last]

		values++
		if current.depth > MaxLiteralNesting || values > MaxLiteralValues {
			return &literalValidationIssue{
				code:   "resource.literal_budget",
				suffix: current.suffix,
				message: fmt.Sprintf(
					"literal must not exceed nesting %d or %d total values",
					MaxLiteralNesting,
					MaxLiteralValues,
				),
			}
		}

		switch typed := current.value.(type) {
		case nil, bool, string,
			int, int8, int16, int32, int64,
			uint, uint8, uint16, uint32, uint64:
			// Normalized JSON scalars.
		case float32:
			if math.IsNaN(float64(typed)) || math.IsInf(float64(typed), 0) {
				return nonFiniteLiteralIssue(current.suffix)
			}
		case float64:
			if math.IsNaN(typed) || math.IsInf(typed, 0) {
				return nonFiniteLiteralIssue(current.suffix)
			}
		case json.Number:
			if _, err := normalizedArtifactNumber(string(typed)); err != nil {
				return &literalValidationIssue{
					code:    "literal.invalid_number",
					suffix:  current.suffix,
					message: "literal number is not a valid finite JSON number",
				}
			}
		case []any:
			// Reverse insertion keeps the first reported issue deterministic by
			// visiting array elements in ascending index order.
			for index := len(typed) - 1; index >= 0; index-- {
				pending = append(pending, pendingLiteral{
					value:  typed[index],
					depth:  current.depth + 1,
					suffix: fmt.Sprintf("%s[%d]", current.suffix, index),
				})
			}
		case map[string]any:
			keys := make([]string, 0, len(typed))
			for key := range typed {
				keys = append(keys, key)
			}
			sort.Strings(keys)
			for index := len(keys) - 1; index >= 0; index-- {
				key := keys[index]
				pending = append(pending, pendingLiteral{
					value:  typed[key],
					depth:  current.depth + 1,
					suffix: current.suffix + "." + key,
				})
			}
		default:
			return &literalValidationIssue{
				code:    "literal.unsupported_type",
				suffix:  current.suffix,
				message: fmt.Sprintf("literal contains unsupported value type %T; use only canonical JSON values", current.value),
			}
		}
	}
	return nil
}

func nonFiniteLiteralIssue(suffix string) *literalValidationIssue {
	return &literalValidationIssue{
		code:    "literal.non_finite_number",
		suffix:  suffix,
		message: "literal number must be finite",
	}
}

func (v *validator) validateLiteral(path string, value any, nodeID, edgeID string) bool {
	issue := validateDeterministicLiteral(value)
	if issue == nil {
		return true
	}
	v.error(issue.code, path+issue.suffix, issue.message, nodeID, edgeID)
	return false
}
