// Package schema JSON Schema'un ihtiyacımız olan küçük bir alt kümesini doğrular ve
// nesne özelliklerine varsayılan değerleri uygular.
//
// Kapsam bilinçli olarak dardır: type, properties, required, additionalProperties,
// items, enum, minimum, maximum, minLength, maxLength, minItems, maxItems, pattern,
// default. Bilinmeyen anahtarlar SESSİZCE yok sayılır: yarım bir şema geçerli sayılır,
// ama yanlış değer de reddedilir (katı mod sonraki fazın işi).
//
// Bu paket tool girdi doğrulamasında ve (Faz 5'te) LLM'in ürettiği tool argümanlarında
// kullanılır. Bağımlılığı yoktur; her iki katman da aynı kuralları uygular.
package schema

import (
	"encoding/json"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"unicode/utf8"
)

// Violation tek bir doğrulama hatasıdır. Path hatayı gösterir ("" = kök/gövde).
// Kullanıcı arayüzü bu alanı form üzerinde işaretleyebilir.
type Violation struct {
	Path    string `json:"path,omitempty"`
	Message string `json:"message"`
}

func (v Violation) String() string {
	if v.Path == "" {
		return v.Message
	}
	return v.Path + ": " + v.Message
}

// Validate şemaya göre değeri doğrular ve TÜM ihlalleri döndürür (arayüz hepsini bir
// seferde gösterebilsin). Şema nil ise doğrulama yapılmaz (serbest bırakılmış alan).
//
// Hata sırası kararlıdır: nesne özellikleri ada göre sıralanır. Aynı girdi için aynı
// mesaj listesi üretilir — testler ve arayüz buna güvenir.
func Validate(s map[string]any, value any) []Violation {
	var out []Violation
	validate(s, value, "", &out)
	return out
}

// ApplyDefaults nesne şemasındaki varsayılanları eksik alanlara doldurur ve DERİN bir
// kopya döndürür (çağıranın girdisi değişmez). Varsayılanı olmayan alanlara dokunulmaz;
// şema "required" ama alan yoksa burada üretilmez (doğrulama bunu zaten bildirir).
func ApplyDefaults(s map[string]any, value any) any {
	m, ok := value.(map[string]any)
	if !ok {
		return value // dizi/ilkel değerlerde varsayılan uygulanmaz
	}
	out := deepCopyMap(m)
	applyDefaults(s, out)
	return out
}

func applyDefaults(s map[string]any, obj map[string]any) {
	props, _ := s["properties"].(map[string]any)
	if props == nil {
		return
	}
	for _, name := range sortedKeys(props) {
		sub, _ := props[name].(map[string]any)
		if sub == nil {
			continue
		}
		cur, present := obj[name]
		if !present || cur == nil {
			if def, ok := sub["default"]; ok {
				obj[name] = def
			}
			continue
		}
		if child, isObj := cur.(map[string]any); isObj {
			applyDefaults(sub, child)
		}
	}
}

func validate(s map[string]any, value any, path string, out *[]Violation) {
	if s == nil {
		return
	}
	add := func(format string, args ...any) {
		*out = append(*out, Violation{Path: path, Message: fmt.Sprintf(format, args...)})
	}

	if t, ok := s["type"]; ok && !typeMatches(t, value) {
		add("tipi %s olmalı (alınan: %s)", typeNames(t), jsonTypeOf(value))
		return // tip tutmuyorsa alt alanları denetmenin anlamı yok
	}
	if enum, ok := s["enum"].([]any); ok && len(enum) > 0 && !containsValue(enum, value) {
		add("şu değerlerden biri olmalı: %s", joinValues(enum))
	}

	switch v := value.(type) {
	case string:
		validateString(s, v, add)
	case map[string]any:
		validateObject(s, v, path, out)
	case []any:
		validateArray(s, v, path, out)
	}

	if f, ok := toFloat(value); ok {
		if m, ok := numberOf(s["minimum"]); ok && f < m {
			*out = append(*out, Violation{Path: path, Message: "en az " + formatNumber(m) + " olmalı"})
		}
		if m, ok := numberOf(s["maximum"]); ok && f > m {
			*out = append(*out, Violation{Path: path, Message: "en fazla " + formatNumber(m) + " olmalı"})
		}
	}
}

func validateString(s map[string]any, v string, add func(string, ...any)) {
	n := float64(utf8.RuneCountInString(v)) // RUNE: Türkçe metinler bayt sayımıyla yanlış ölçülür
	if m, ok := numberOf(s["minLength"]); ok && n < m {
		add("en az %s karakter olmalı", formatNumber(m))
	}
	if m, ok := numberOf(s["maxLength"]); ok && n > m {
		add("en fazla %s karakter olmalı", formatNumber(m))
	}
	if p, ok := s["pattern"].(string); ok && p != "" {
		// Geçersiz desen şemayı değil GİRDİYİ reddetmemeli: şema hatası yok sayılır.
		if re, err := regexp.Compile(p); err == nil && !re.MatchString(v) {
			add("%q desenine uymuyor", p)
		}
	}
}

// validateObject nesne şemasını denetler. Hatalar ALAN ADINA GÖRE SIRALI üretilir:
// arayüz listeyi olduğu gibi basar, "zorunlu alan" hatasının en sonda kalması
// kullanıcıyı formun sonuna kadar gezindirirdi.
func validateObject(s map[string]any, obj map[string]any, path string, out *[]Violation) {
	props, _ := s["properties"].(map[string]any)
	required := map[string]bool{}
	for _, r := range stringList(s["required"]) {
		required[r] = true
	}

	names := make([]string, 0, len(obj)+len(required))
	names = append(names, sortedKeys(obj)...)
	for r := range required {
		if _, present := obj[r]; !present {
			names = append(names, r)
		}
	}
	sort.Strings(names)

	for _, name := range names {
		value, present := obj[name]
		if !present {
			*out = append(*out, Violation{Path: join(path, name), Message: "zorunlu alan"})
			continue
		}
		sub, _ := props[name].(map[string]any)
		if sub == nil {
			if closed, isClosed := s["additionalProperties"].(bool); isClosed && !closed {
				*out = append(*out, Violation{Path: join(path, name), Message: "tanımsız alan"})
			}
			continue
		}
		validate(sub, value, join(path, name), out)
	}
}

func validateArray(s map[string]any, arr []any, path string, out *[]Violation) {
	if m, ok := numberOf(s["minItems"]); ok && float64(len(arr)) < m {
		*out = append(*out, Violation{Path: path, Message: "en az " + formatNumber(m) + " öğe olmalı"})
	}
	if m, ok := numberOf(s["maxItems"]); ok && float64(len(arr)) > m {
		*out = append(*out, Violation{Path: path, Message: "en fazla " + formatNumber(m) + " öğe olmalı"})
	}
	items, _ := s["items"].(map[string]any)
	for i, v := range arr {
		validate(items, v, fmt.Sprintf("%s[%d]", path, i), out)
	}
}

// ---- yardımcılar ----

// join alt yol üretir: kök "" ise "ad", iç içe ise "üst.iç".
func join(path, name string) string {
	if path == "" {
		return name
	}
	return path + "." + name
}

func sortedKeys[V any](m map[string]V) []string {
	out := make([]string, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	sort.Strings(out) // kararlı sıra: hata listesi testte de arayüzde de aynı
	return out
}

func deepCopyMap(m map[string]any) map[string]any {
	out := make(map[string]any, len(m))
	for k, v := range m {
		out[k] = deepCopyValue(v)
	}
	return out
}

func deepCopyValue(v any) any {
	switch t := v.(type) {
	case map[string]any:
		return deepCopyMap(t)
	case []any:
		out := make([]any, len(t))
		for i, x := range t {
			out[i] = deepCopyValue(x)
		}
		return out
	default:
		return v
	}
}

// stringList şemadaki "required" alanını []string'e çevirir (elle yazılmış şemada
// []any gelmesi normal).
func stringList(v any) []string {
	switch t := v.(type) {
	case []string:
		return t
	case []any:
		out := make([]string, 0, len(t))
		for _, x := range t {
			if s, ok := x.(string); ok {
				out = append(out, s)
			}
		}
		return out
	}
	return nil
}

func containsValue(list []any, v any) bool {
	for _, x := range list {
		if equalJSON(x, v) {
			return true
		}
	}
	return false
}

// equalJSON iki değeri JSON üzerinden karşılaştırır: int 1 ile float64 1 ve bool true
// birbirine eşit sayılır (şemada enum elle yazıldığında tip kaybı normaldir).
func equalJSON(a, b any) bool {
	ab, err1 := json.Marshal(a)
	bb, err2 := json.Marshal(b)
	return err1 == nil && err2 == nil && string(ab) == string(bb)
}

func joinValues(list []any) string {
	parts := make([]string, 0, len(list))
	for _, v := range list {
		b, err := json.Marshal(v)
		if err != nil {
			parts = append(parts, fmt.Sprint(v))
			continue
		}
		parts = append(parts, string(b))
	}
	return strings.Join(parts, ", ")
}

func typeNames(t any) string {
	if names, ok := t.([]any); ok {
		parts := make([]string, 0, len(names))
		for _, x := range names {
			parts = append(parts, fmt.Sprint(x))
		}
		return strings.Join(parts, " veya ")
	}
	if s, ok := t.(string); ok {
		return s
	}
	return "geçerli"
}

func typeMatches(t, value any) bool {
	if names, ok := t.([]any); ok {
		for _, x := range names {
			if name, ok := x.(string); ok && matchesType(name, value) {
				return true
			}
		}
		return false
	}
	name, ok := t.(string)
	if !ok {
		return true // şema geçersiz: engelleme, doğrulama aşağıda devam eder
	}
	return matchesType(name, value)
}

func matchesType(name string, value any) bool {
	switch name {
	case "object":
		_, ok := value.(map[string]any)
		return ok
	case "array":
		_, ok := value.([]any)
		return ok
	case "string":
		_, ok := value.(string)
		return ok
	case "boolean":
		_, ok := value.(bool)
		return ok
	case "null":
		return value == nil
	case "number":
		_, ok := toFloat(value)
		return ok
	case "integer":
		f, ok := toFloat(value)
		return ok && f == float64(int64(f)) // 3.5 "integer" değildir
	}
	return true // bilinmeyen tip adı: ileri uyumluluk, engelleme yok
}

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

// toFloat sayısal değeri float64'e çevirir. bool sayı DEĞİLDİR (true = 1 kabul edilmez).
func toFloat(v any) (float64, bool) {
	switch n := v.(type) {
	case float64:
		return n, true
	case float32:
		return float64(n), true
	case int:
		return float64(n), true
	case int32:
		return float64(n), true
	case int64:
		return float64(n), true
	case uint:
		return float64(n), true
	case uint64:
		return float64(n), true
	case json.Number:
		f, err := n.Float64()
		return f, err == nil
	}
	return 0, false
}

// numberOf şemadaki sayıyı okur. Elle yazılmış şemada sayı yerine metin geldiyse
// ("5") kabul edilir; okunamayan değer "kısıt yok" sayılır.
func numberOf(v any) (float64, bool) {
	if f, ok := toFloat(v); ok {
		return f, true
	}
	if s, ok := v.(string); ok {
		if f, err := strconv.ParseFloat(strings.TrimSpace(s), 64); err == nil {
			return f, true
		}
	}
	return 0, false
}

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