package jsonschema

import (
	"reflect"
	"strconv"
	"strings"
	"unicode/utf8"
)

// Validate reports all violations of s against v. v is the decoded data —
// typically the result of json.Unmarshal or yaml.Unmarshal into `any`.
//
// Returns nil when v conforms. Returns one or more Error entries otherwise.
// Errors carry JSON-Pointer paths so the form renderer can attach each
// message to the right widget.
//
// Supported keywords (v0 subset): type, properties, required, items, enum,
// minimum, maximum, minLength, maxLength, format (date, email),
// additionalProperties: false. Everything else is silently ignored — the
// form-spec meta-schema enforces that authors stay within this subset.
func Validate(s *Schema, v any) []Error {
	if s == nil {
		return nil
	}
	var errs []Error
	validate(s, v, "", &errs)
	return errs
}

func validate(s *Schema, v any, path string, errs *[]Error) {
	if s == nil {
		return
	}

	// Type check first — a wrong-typed value can't satisfy the rest, and
	// dispatching the constraint checks below assumes the type matches.
	if s.Type != "" && !typeMatches(s.Type, v) {
		*errs = append(*errs, Error{
			Path:    path,
			Message: typeMessage(s.Type, v),
		})
		return
	}

	// enum: value must be one of the listed alternatives. Numeric comparisons
	// coerce int↔float to handle JSON's float64 vs YAML's int mismatch.
	if len(s.Enum) > 0 && !enumContains(s.Enum, v) {
		*errs = append(*errs, Error{
			Path:    path,
			Message: "must be one of the allowed values",
		})
		return
	}

	switch s.Type {
	case "string":
		validateString(s, v.(string), path, errs)
	case "number", "integer":
		validateNumber(s, v, path, errs)
	case "object":
		validateObject(s, v, path, errs)
	case "array":
		validateArray(s, v, path, errs)
	}
}

func validateString(s *Schema, str, path string, errs *[]Error) {
	n := utf8.RuneCountInString(str)
	if s.MinLength != nil && n < *s.MinLength {
		*errs = append(*errs, Error{Path: path, Message: minLenMsg(*s.MinLength)})
	}
	if s.MaxLength != nil && n > *s.MaxLength {
		*errs = append(*errs, Error{Path: path, Message: maxLenMsg(*s.MaxLength)})
	}
	if s.Format != "" && !formatValid(s.Format, str) {
		*errs = append(*errs, Error{Path: path, Message: "must be a valid " + s.Format})
	}
}

func validateNumber(s *Schema, v any, path string, errs *[]Error) {
	f, _ := toFloat(v)
	if s.Minimum != nil && f < *s.Minimum {
		*errs = append(*errs, Error{Path: path, Message: "must be ≥ " + numStr(*s.Minimum)})
	}
	if s.Maximum != nil && f > *s.Maximum {
		*errs = append(*errs, Error{Path: path, Message: "must be ≤ " + numStr(*s.Maximum)})
	}
}

func validateObject(s *Schema, v any, path string, errs *[]Error) {
	obj, ok := v.(map[string]any)
	if !ok {
		return
	}

	for _, name := range s.Required {
		if _, present := obj[name]; !present {
			*errs = append(*errs, Error{
				Path:    ptrPush(path, name),
				Message: "required",
			})
		}
	}

	for name, propSchema := range s.Properties {
		if val, present := obj[name]; present {
			validate(propSchema, val, ptrPush(path, name), errs)
		}
	}

	// additionalProperties: false → reject anything not declared in properties.
	// Only the bool-false form is enforced in v0; schema form is parsed but ignored.
	if ap, ok := s.AdditionalProperties.(bool); ok && !ap {
		for name := range obj {
			if _, declared := s.Properties[name]; !declared {
				*errs = append(*errs, Error{
					Path:    ptrPush(path, name),
					Message: "unexpected property",
				})
			}
		}
	}
}

func validateArray(s *Schema, v any, path string, errs *[]Error) {
	arr, ok := v.([]any)
	if !ok {
		return
	}
	if s.Items != nil {
		for i, item := range arr {
			validate(s.Items, item, ptrPushIdx(path, i), errs)
		}
	}
}

// --- Type / coercion helpers -------------------------------------------------

func typeMatches(t string, v any) bool {
	switch t {
	case "null":
		return v == nil
	case "boolean":
		_, ok := v.(bool)
		return ok
	case "string":
		_, ok := v.(string)
		return ok
	case "number":
		_, ok := toFloat(v)
		return ok
	case "integer":
		if _, ok := toFloat(v); !ok {
			return false
		}
		return isInteger(v)
	case "array":
		_, ok := v.([]any)
		return ok
	case "object":
		_, ok := v.(map[string]any)
		return ok
	}
	return true
}

func toFloat(v any) (float64, bool) {
	switch n := v.(type) {
	case int:
		return float64(n), true
	case int8:
		return float64(n), true
	case int16:
		return float64(n), true
	case int32:
		return float64(n), true
	case int64:
		return float64(n), true
	case uint:
		return float64(n), true
	case uint8:
		return float64(n), true
	case uint16:
		return float64(n), true
	case uint32:
		return float64(n), true
	case uint64:
		return float64(n), true
	case float32:
		return float64(n), true
	case float64:
		return n, true
	}
	return 0, false
}

func isInteger(v any) bool {
	switch n := v.(type) {
	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
		return true
	case float32:
		f := float64(n)
		return f == float64(int64(f))
	case float64:
		return n == float64(int64(n))
	}
	return false
}

// enumContains returns true when v matches one of the enum values, with
// numeric coercion (int↔float) so that JSON's float64-default doesn't reject
// an enum authored in YAML as plain integers.
func enumContains(opts []any, v any) bool {
	for _, opt := range opts {
		if valuesEqual(opt, v) {
			return true
		}
	}
	return false
}

func valuesEqual(a, b any) bool {
	af, aok := toFloat(a)
	bf, bok := toFloat(b)
	if aok && bok {
		return af == bf
	}
	return reflect.DeepEqual(a, b)
}

// --- JSON Pointer (RFC 6901) -------------------------------------------------

func ptrPush(path, segment string) string {
	return path + "/" + ptrEnc(segment)
}

func ptrPushIdx(path string, idx int) string {
	return path + "/" + strconv.Itoa(idx)
}

func ptrEnc(s string) string {
	s = strings.ReplaceAll(s, "~", "~0")
	s = strings.ReplaceAll(s, "/", "~1")
	return s
}

// --- Message formatting ------------------------------------------------------

func typeMessage(want string, got any) string {
	return "expected " + want + ", got " + typeName(got)
}

func typeName(v any) string {
	switch v.(type) {
	case nil:
		return "null"
	case bool:
		return "boolean"
	case string:
		return "string"
	case []any:
		return "array"
	case map[string]any:
		return "object"
	}
	if _, ok := toFloat(v); ok {
		return "number"
	}
	return "unknown"
}

func minLenMsg(n int) string { return "must be at least " + strconv.Itoa(n) + " characters" }
func maxLenMsg(n int) string { return "must be at most " + strconv.Itoa(n) + " characters" }

func numStr(f float64) string {
	return strconv.FormatFloat(f, 'g', -1, 64)
}
