package knowledge

import (
	"context"
	"sort"
	"strings"
	"sync"
)

// MemoryRepo bellek içi Repository'dir (MONGO_ENABLED=false / Mongo erişilemezse düşüş + testler).
// Eşzamanlı kullanıma güvenlidir; dışarı verilen her değer KOPYADIR.
type MemoryRepo struct {
	mu     sync.RWMutex
	docs   map[string]Document
	chunks map[string]Chunk
}

var _ Repository = (*MemoryRepo)(nil)

// NewMemoryRepo boş bir bellek deposu üretir.
func NewMemoryRepo() *MemoryRepo {
	return &MemoryRepo{docs: map[string]Document{}, chunks: map[string]Chunk{}}
}

func (r *MemoryRepo) CreateDocument(_ context.Context, d *Document) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, exists := r.docs[d.ID]; exists {
		return ErrDuplicate
	}
	for _, o := range r.docs {
		if o.UserID == d.UserID && o.Hash == d.Hash {
			return ErrDuplicate
		}
	}
	r.docs[d.ID] = d.Clone()
	return nil
}

func (r *MemoryRepo) GetDocument(_ context.Context, id string) (*Document, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	d, ok := r.docs[id]
	if !ok {
		return nil, ErrNotFound
	}
	c := d.Clone()
	return &c, nil
}

func (r *MemoryRepo) GetDocumentByHash(_ context.Context, userID, hash string) (*Document, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	for _, d := range r.docs {
		if d.UserID == userID && d.Hash == hash {
			c := d.Clone()
			return &c, nil
		}
	}
	return nil, ErrNotFound
}

func (r *MemoryRepo) UpdateDocument(_ context.Context, d *Document) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ok := r.docs[d.ID]; !ok {
		return ErrNotFound
	}
	r.docs[d.ID] = d.Clone()
	return nil
}

func (r *MemoryRepo) DeleteDocument(_ context.Context, id string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ok := r.docs[id]; !ok {
		return ErrNotFound
	}
	r.deleteChunksLocked(id)
	delete(r.docs, id)
	return nil
}

func (r *MemoryRepo) deleteChunksLocked(documentID string) {
	for id, c := range r.chunks {
		if c.DocumentID == documentID {
			delete(r.chunks, id)
		}
	}
}

func matchDocument(d Document, f DocumentFilter) bool {
	if d.UserID != f.UserID {
		return false
	}
	if f.ProjectID != "" && d.ProjectID != f.ProjectID {
		return false
	}
	if f.Status != "" && d.Status != f.Status {
		return false
	}
	if f.Tag != "" && !contains(d.Tags, f.Tag) {
		return false
	}
	if q := strings.ToLower(strings.TrimSpace(f.Query)); q != "" {
		if !strings.Contains(strings.ToLower(d.Title), q) && !strings.Contains(strings.ToLower(d.Source), q) {
			return false
		}
	}
	return true
}

func contains(xs []string, s string) bool {
	for _, x := range xs {
		if x == s {
			return true
		}
	}
	return false
}

func (r *MemoryRepo) ListDocuments(_ context.Context, f DocumentFilter, limit, offset int) ([]Document, error) {
	r.mu.RLock()
	all := make([]Document, 0, len(r.docs))
	for _, d := range r.docs {
		if matchDocument(d, f) {
			all = append(all, d.Clone())
		}
	}
	r.mu.RUnlock()

	sort.Slice(all, func(i, j int) bool {
		if !all[i].CreatedAt.Equal(all[j].CreatedAt) {
			return all[i].CreatedAt.After(all[j].CreatedAt)
		}
		return all[i].ID < all[j].ID
	})
	if offset < 0 {
		offset = 0
	}
	if offset >= len(all) {
		return []Document{}, nil
	}
	all = all[offset:]
	if limit > 0 && len(all) > limit {
		all = all[:limit]
	}
	return all, nil
}

func (r *MemoryRepo) CountDocuments(_ context.Context, f DocumentFilter) (int, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	n := 0
	for _, d := range r.docs {
		if matchDocument(d, f) {
			n++
		}
	}
	return n, nil
}

func (r *MemoryRepo) PendingDocuments(_ context.Context) ([]Document, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	out := make([]Document, 0)
	for _, d := range r.docs {
		if d.Status == StatusIndexing {
			out = append(out, d.Clone())
		}
	}
	sort.Slice(out, func(i, j int) bool {
		if !out[i].CreatedAt.Equal(out[j].CreatedAt) {
			return out[i].CreatedAt.Before(out[j].CreatedAt) // en eski önce
		}
		return out[i].ID < out[j].ID
	})
	return out, nil
}

func (r *MemoryRepo) AddChunks(_ context.Context, chunks []Chunk) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, c := range chunks {
		if _, exists := r.chunks[c.ID]; exists {
			return ErrDuplicate
		}
	}
	for _, c := range chunks {
		r.chunks[c.ID] = c.Clone()
	}
	return nil
}

func (r *MemoryRepo) ChunksOfDocument(_ context.Context, documentID string, limit, offset int) ([]Chunk, error) {
	r.mu.RLock()
	out := make([]Chunk, 0)
	for _, c := range r.chunks {
		if c.DocumentID == documentID {
			out = append(out, c.Clone())
		}
	}
	r.mu.RUnlock()

	sort.Slice(out, func(i, j int) bool { return out[i].Index < out[j].Index })
	if offset < 0 {
		offset = 0
	}
	if offset >= len(out) {
		return []Chunk{}, nil
	}
	out = out[offset:]
	if limit > 0 && len(out) > limit {
		out = out[:limit]
	}
	return out, nil
}

func (r *MemoryRepo) SearchableChunks(_ context.Context, f ChunkFilter) ([]Chunk, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	out := make([]Chunk, 0)
	for _, c := range r.chunks {
		if c.UserID != f.UserID {
			continue
		}
		if f.ProjectID != "" && c.ProjectID != f.ProjectID {
			continue
		}
		if len(f.DocumentIDs) > 0 && !contains(f.DocumentIDs, c.DocumentID) {
			continue
		}
		if len(f.Tags) > 0 && !anyOf(c.Tags, f.Tags) {
			continue
		}
		out = append(out, c.Clone())
	}
	return out, nil
}

func anyOf(have, want []string) bool {
	for _, w := range want {
		if contains(have, w) {
			return true
		}
	}
	return false
}

func (r *MemoryRepo) SetChunkEmbedding(_ context.Context, chunkID string, vec []float32, model string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	c, ok := r.chunks[chunkID]
	if !ok {
		return ErrNotFound
	}
	c.Embedding = cloneSlice(vec)
	c.EmbedModel = model
	r.chunks[chunkID] = c
	return nil
}

func (r *MemoryRepo) DeleteChunks(_ context.Context, documentID string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.deleteChunksLocked(documentID)
	return nil
}
