package claim

import (
	"encoding/json"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	"fiko-ai/internal/claim/kinds"
)

// FieldMap modelin "fields" nesnesidir. Model değerleri sayı/bool yazabilir: bunlar metne çevrilir;
// iç içe nesne/dizi değerleri atılır. Hatalı biçim TÜM adayı bozmaz (alanlar boş sayılır).
type FieldMap map[string]string

// UnmarshalJSON toleranslı ayrıştırır.
func (m *FieldMap) UnmarshalJSON(b []byte) error {
	var raw map[string]any
	if err := json.Unmarshal(b, &raw); err != nil {
		*m = nil
		return nil
	}
	out := make(FieldMap, len(raw))
	for k, v := range raw {
		switch x := v.(type) {
		case string:
			out[k] = x
		case float64:
			out[k] = strconv.FormatFloat(x, 'f', -1, 64)
		case bool:
			out[k] = strconv.FormatBool(x)
		}
	}
	*m = out
	return nil
}

// ParseCandidates model çıktısını TOLERANSLI ayrıştırır: ```json çitleri, {"remember":[...]} nesnesi
// ve çıplak [...] dizisi kabul edilir. Çözümlenemezse nil döner. Burada süzme YAPILMAZ (bkz. Clean).
func ParseCandidates(raw string) []Candidate {
	s := strings.TrimSpace(raw)
	s = strings.TrimPrefix(s, "```json")
	s = strings.TrimPrefix(s, "```")
	s = strings.TrimSuffix(s, "```")
	s = strings.TrimSpace(s)

	// Önce nesne biçimi: {"remember":[...]}
	if i, j := strings.Index(s, "{"), strings.LastIndex(s, "}"); i >= 0 && j > i {
		var wrap struct {
			Remember []Candidate `json:"remember"`
		}
		if err := json.Unmarshal([]byte(s[i:j+1]), &wrap); err == nil && wrap.Remember != nil {
			return wrap.Remember
		}
	}
	// Sonra çıplak dizi: [...]
	if i, j := strings.Index(s, "["), strings.LastIndex(s, "]"); i >= 0 && j > i {
		var items []Candidate
		if err := json.Unmarshal([]byte(s[i:j+1]), &items); err == nil {
			return items
		}
	}
	return nil
}

// Clean adayları yazılabilir hâle getirir. Kullanıcıya ait bilgiyi DEĞİŞTİRMEZ, yalnızca biçimler:
//   - text: boşluklar normalleştirilir, MaxTextRunes'a kırpılır; boş olanlar atılır.
//   - kind: Türkçe katlanır ("Kişi" → "kisi"), geçersizse "genel".
//   - key: NormalizeKey ile normalleştirilir.
//
// Listelik türde (firma, kisi, urun) adı (key) olmayan aday yazılamaz: ikinci dilime (incomplete) girer.
func Clean(in []Candidate) (ok, incomplete []Candidate) { return CleanAt(in, time.Now()) }

// CleanAt Clean gibidir; göreli tarihler ("cuma", "ay sonu") now'a göre çözülür. Tipli türlerde
// (kinds şeması) alanlar kanonik biçime çevrilir, birleştirme anahtarı yoksa şemanın ad alanından
// türetilir; ZORUNLU alan eksik/geçersizse aday yazılamaz ve ikinci dilime (eksik bilgi) girer.
func CleanAt(in []Candidate, now time.Time) (ok, incomplete []Candidate) {
	for _, c := range in {
		text := clipRunes(NormalizeText(c.Text), MaxTextRunes)
		if text == "" {
			continue
		}
		cand := Candidate{
			Text: text,
			Kind: normalizeKind(c.Kind),
			Key:  strings.TrimSpace(clipRunes(NormalizeKey(c.Key), MaxKeyRunes)),
		}
		if sc, typed := kinds.Default().Get(cand.Kind); typed && len(sc.Fields) > 0 {
			clean, missing := sc.NormalizeKeyed(c.Fields, strings.TrimSpace(c.Key), now)
			if len(missing) > 0 {
				incomplete = append(incomplete, cand)
				continue
			}
			if len(clean) > 0 {
				cand.Fields = clean
			}
			if cand.Key == "" && sc.KeyField != "" {
				cand.Key = strings.TrimSpace(clipRunes(NormalizeKey(clean[sc.KeyField]), MaxKeyRunes))
			}
		}
		if IsListKind(cand.Kind) && cand.Key == "" {
			incomplete = append(incomplete, cand)
			continue
		}
		ok = append(ok, cand)
	}
	return ok, incomplete
}

// normalizeKind tür etiketini geçerli biçime getirir: Türkçe katlama, küçük harf, boşluk → "_".
func normalizeKind(k string) string {
	k = strings.ToLower(turkishFold.Replace(strings.TrimSpace(k)))
	k = strings.Join(strings.Fields(k), "_")
	if !kindPattern.MatchString(k) {
		return KindGeneral
	}
	return k
}

// SplitChunks metni satır sınırlarında, her biri en çok max karakter olan parçalara böler.
// Hiçbir içerik kaybolmaz; max'tan uzun tek bir satır boşlukta (yoksa tam max'ta) bölünür.
func SplitChunks(text string, max int) []string {
	text = strings.TrimSpace(strings.ReplaceAll(text, "\r\n", "\n"))
	if text == "" || max <= 0 {
		return nil
	}
	var chunks []string
	var cur strings.Builder
	curRunes := 0
	flush := func() {
		if s := strings.TrimSpace(cur.String()); s != "" {
			chunks = append(chunks, s)
		}
		cur.Reset()
		curRunes = 0
	}
	for _, line := range strings.Split(text, "\n") {
		line = strings.TrimRight(line, " \t\r")
		rs := []rune(line)
		for len(rs) > max {
			flush()
			cut := max
			for i := max; i > max/2; i-- { // en yakın boşlukta kes
				if rs[i-1] == ' ' {
					cut = i
					break
				}
			}
			if s := strings.TrimSpace(string(rs[:cut])); s != "" {
				chunks = append(chunks, s)
			}
			rs = rs[cut:]
		}
		line = string(rs)
		n := utf8.RuneCountInString(line)
		if curRunes > 0 && curRunes+n+1 > max {
			flush()
		}
		cur.WriteString(line)
		cur.WriteByte('\n')
		curRunes += n + 1
	}
	flush()
	return chunks
}

// clipRunes metni en çok n karaktere (bayta değil) keser.
func clipRunes(s string, n int) string {
	if utf8.RuneCountInString(s) <= n {
		return s
	}
	return string([]rune(s)[:n])
}
