// Package knowledge Bilgi Tabanı (RAG) modülüdür (Faz 8).
//
// Akış (PROJE.MD "RAG"):
//
//	belge → parser → chunker → embedding → depo
//	sorgu → embedding → vektör arama → yeniden sıralama → bağlam
//
// Skill vektörlerinden AYRIDIR (spec: "Knowledge Base ile Skill Vector Store birbirinden
// ayrılmalı"): ayrı koleksiyonlar (documents, chunks), ayrı servis, ayrı arama.
// Belgeler kullanıcıya aittir; başka kullanıcının belgesi ne aranır ne okunur.
package knowledge

import (
	"strings"
	"time"
	"unicode/utf8"
)

// Status belgenin işlenme durumudur.
type Status string

const (
	// StatusIndexing parçalar kaydedildi, embedding arka planda üretiliyor.
	// Bu aşamada belge anahtar kelimeyle zaten aranabilir.
	StatusIndexing Status = "indexing"
	// StatusReady embedding işi bitti. EmbeddedCount < ChunkCount ise bazı parçalar
	// embedding'siz kaldı (sağlayıcı hatası); Reindex ile tamamlanır, arama yine çalışır.
	StatusReady Status = "ready"
)

// Valid bilinen bir durum mu?
func (s Status) Valid() bool { return s == StatusIndexing || s == StatusReady }

// Sınırlar. Üst sınırlar kötü niyetli/yanlış girdinin kayıt şişirmesini önler.
const (
	MaxTitleRunes   = 200
	MaxSourceRunes  = 500
	MaxProjectRunes = 100
	MaxTags         = 20
	MaxTagRunes     = 40
)

// Document bir bilgi tabanı belgesidir (PROJE.MD metadata: documentId, source, title, tags,
// userId, projectId, createdAt). Metnin kendisi burada tutulmaz; parçalarda (Chunk) yaşar.
type Document struct {
	ID        string `bson:"_id" json:"id"`
	UserID    string `bson:"userId" json:"userId"`
	ProjectID string `bson:"projectId,omitempty" json:"projectId,omitempty"`

	Title       string   `bson:"title" json:"title"`
	Source      string   `bson:"source,omitempty" json:"source,omitempty"`           // URL, dosya yolu, "elle girildi" vb.
	Filename    string   `bson:"filename,omitempty" json:"filename,omitempty"`       // yüklenen dosyanın adı
	ContentType string   `bson:"contentType,omitempty" json:"contentType,omitempty"` // yüklenen içeriğin türü
	Tags        []string `bson:"tags" json:"tags"`

	// Hash, normalize edilmiş metnin SHA-256'sıdır; aynı kullanıcının aynı içeriği iki kez
	// yüklemesini engeller (kayıtta benzersiz index).
	Hash string `bson:"hash" json:"hash"`

	SizeBytes     int    `bson:"sizeBytes" json:"sizeBytes"`         // ayrıştırılmış metnin bayt boyutu
	Status        Status `bson:"status" json:"status"`               // indexing | ready
	ChunkCount    int    `bson:"chunkCount" json:"chunkCount"`       // toplam parça
	EmbeddedCount int    `bson:"embeddedCount" json:"embeddedCount"` // embedding'i hazır parça

	CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
	UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
}

// Clone derin kopya döndürür; depolar ve çağıranlar birbirinin verisini değiştiremesin.
func (d Document) Clone() Document {
	c := d
	c.Tags = cloneSlice(d.Tags)
	return c
}

// Chunk belgenin aranabilir tek parçasıdır. Metadata belgeden kopyalanır: arama,
// belgeyi ayrıca okumadan filtreleyebilsin (userId, projectId, tags).
type Chunk struct {
	ID         string `bson:"_id" json:"id"`
	DocumentID string `bson:"documentId" json:"documentId"`
	UserID     string `bson:"userId" json:"userId"`
	ProjectID  string `bson:"projectId,omitempty" json:"projectId,omitempty"`

	Index  int      `bson:"index" json:"index"` // belge içindeki sıra (0'dan)
	Text   string   `bson:"text" json:"text"`
	Title  string   `bson:"title" json:"title"`
	Source string   `bson:"source,omitempty" json:"source,omitempty"`
	Tags   []string `bson:"tags" json:"tags"`

	// Embedding vektör aramada kullanılır; API'de dönmez (büyük ve işe yaramaz).
	// EmbedModel vektörü üreten modeldir: farklı modellerin vektörleri aynı boyutta olsa bile
	// karşılaştırılamaz, bu yüzden arama yalnızca güncel modelin vektörlerini kullanır.
	Embedding  []float32 `bson:"embedding,omitempty" json:"-"`
	EmbedModel string    `bson:"embedModel,omitempty" json:"-"`

	CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
}

// Clone derin kopya döndürür.
func (c Chunk) Clone() Chunk {
	out := c
	out.Tags = cloneSlice(c.Tags)
	out.Embedding = cloneSlice(c.Embedding)
	return out
}

// HasEmbedding parça verilen modelin vektörünü taşıyor mu?
// model boşsa model denetimi yapılmaz (embedding var mı yalnızca).
func (c Chunk) HasEmbedding(model string) bool {
	if len(c.Embedding) == 0 {
		return false
	}
	return model == "" || c.EmbedModel == model
}

// DocumentFilter liste/sayım sorgusunu daraltır; UserID zorunludur (başkasının belgesi görünmez).
type DocumentFilter struct {
	UserID    string
	ProjectID string // boş = tüm projeler
	Status    Status // boş = tüm durumlar
	Tag       string // tam eşleşme (küçük harf)
	Query     string // başlık/kaynakta büyük-küçük harf duyarsız alt dize
}

// ChunkFilter arama adaylarını daraltır; UserID zorunludur.
type ChunkFilter struct {
	UserID      string
	ProjectID   string   // boş = tüm projeler
	DocumentIDs []string // boş = tüm belgeler
	Tags        []string // boş = etiket filtresi yok; doluysa herhangi biri eşleşmeli
}

// FieldError tek bir alanın doğrulama hatasıdır (skill.FieldError ile aynı JSON sözleşmesi).
type FieldError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

// ValidationError bir ya da daha çok alan hatasını taşır.
type ValidationError struct{ Errors []FieldError }

func (e *ValidationError) Error() string {
	if len(e.Errors) == 0 {
		return "knowledge: doğrulama hatası"
	}
	parts := make([]string, 0, len(e.Errors))
	for _, fe := range e.Errors {
		parts = append(parts, fe.Field+": "+fe.Message)
	}
	return "knowledge: geçersiz: " + strings.Join(parts, "; ")
}

// normalizeTags etiketleri küçük harfe çevirir, kırpar, tekrarları atar (sıra korunur).
// Sınır ve uzunluk hataları FieldError olarak döner.
func normalizeTags(in []string) ([]string, []FieldError) {
	out := make([]string, 0, len(in))
	seen := make(map[string]struct{}, len(in))
	var errs []FieldError
	for _, t := range in {
		t = strings.ToLower(strings.TrimSpace(t))
		if t == "" {
			continue
		}
		if utf8.RuneCountInString(t) > MaxTagRunes {
			errs = append(errs, FieldError{Field: "tags", Message: "etiket en fazla 40 karakter olabilir"})
			continue
		}
		if _, dup := seen[t]; dup {
			continue
		}
		seen[t] = struct{}{}
		out = append(out, t)
	}
	if len(out) > MaxTags {
		errs = append(errs, FieldError{Field: "tags", Message: "en fazla 20 etiket verilebilir"})
	}
	return out, errs
}

// cloneSlice nil'i nil, boşu boş korur (JSON'da null ile [] farkı korunur).
func cloneSlice[T any](in []T) []T {
	if in == nil {
		return nil
	}
	out := make([]T, len(in))
	copy(out, in)
	return out
}
