package skill

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strconv"
	"time"

	"fiko-ai/internal/event"
	"fiko-ai/internal/llm"
	"fiko-ai/pkg/id"
)

// maxCopyAttempts "_copy", "_copy2", ... denemelerinin üst sınırı (sonsuz döngü olmasın).
const maxCopyAttempts = 100

// Service Skill Registry'dir: kayıt, güncelleme, silme, listeleme ve doğrulama iş kuralları.
// Depolama Repository'dedir; bu katman normalleştirme, doğrulama, kimlik/zaman damgası ve olay yayınından sorumludur.
// (ExecuteSkill Faz 4-5'te eklenir. Arama ve embedding için search.go'ya bak.)
type Service struct {
	repo      Repository
	bus       event.Bus    // nil olabilir
	embedder  llm.Embedder // nil olabilir: embedding üretilmez, arama anahtar kelimeyle çalışır
	toolExec  ToolExecutor // nil olabilir: ExecuteSkill devre dışı
	toolCheck ToolChecker  // nil olabilir: executorConfig.tool varlığı denetlenmez
	log       *slog.Logger
	now       func() time.Time
	// cache, embedding sonuçlarını kısa süre tutar. Her mesaj skill retrieval
	// çalıştırdığı için (agent.plan) aynı sorgunun tekrar tekrar Ollama'ya
	// gitmesini engeller (bkz. search.go).
	cache *embedCache
}

// NewService Registry kurar. bus nil olabilir (olay yayınlanmaz).
// Embedding için SetEmbedder çağır; çağrılmazsa skill'ler embedding'siz kaydedilir.
func NewService(repo Repository, bus event.Bus) *Service {
	return &Service{
		repo:  repo,
		bus:   bus,
		log:   slog.Default(),
		now:   func() time.Time { return time.Now().UTC() },
		cache: newEmbedCache(nil),
	}
}

// Register yeni skill kaydeder. Kimlik, zaman damgaları ve embedding sunucuya aittir; girdideki değerler yok sayılır.
// Durum boşsa draft olur. Geçersizse *ValidationError, ad çakışırsa ErrDuplicateName döner.
// Embedding üretilemezse kayıt YİNE DE başarılı olur (bkz. embedSkill).
func (s *Service) Register(ctx context.Context, in Skill) (*Skill, error) {
	sk := in.Clone()
	sk.ID = id.New("skill")
	sk.Embedding = nil
	Normalize(&sk)
	errs := append(Validate(&sk), s.checkExecutorTool(ctx, &sk, nil)...)
	if len(errs) > 0 {
		return nil, &ValidationError{Errors: errs}
	}
	sk.Embedding = s.embedSkill(ctx, &sk)
	now := s.now()
	sk.CreatedAt, sk.UpdatedAt = now, now
	if err := s.repo.Create(ctx, &sk); err != nil {
		return nil, err
	}
	s.publish(event.SkillCreated, &sk)
	return &sk, nil
}

// Update skill'i verilen alanlarla TAMAMEN değiştirir (kimlik, oluşturma zamanı ve embedding korunur).
func (s *Service) Update(ctx context.Context, skillID string, in Skill) (*Skill, error) {
	existing, err := s.repo.Get(ctx, skillID)
	if err != nil {
		return nil, err
	}
	sk := in.Clone()
	sk.ID = existing.ID
	sk.CreatedAt = existing.CreatedAt
	sk.Embedding = existing.Embedding
	Normalize(&sk)
	if errs := append(Validate(&sk), s.checkExecutorTool(ctx, &sk, nil)...); len(errs) > 0 {
		return nil, &ValidationError{Errors: errs}
	}
	// Embedding'e giren metin değiştiyse (ya da hiç yoksa) yeniden üret. Üretilemezse ESKİ vektör tutulmaz:
	// eski metne ait vektör yeni metni yanlış temsil eder; nil bırakılır, Reindex doldurur.
	if s.embedder != nil && (len(existing.Embedding) == 0 || EmbeddingText(existing) != EmbeddingText(&sk)) {
		sk.Embedding = s.embedSkill(ctx, &sk)
	}
	sk.UpdatedAt = s.now()
	if err := s.repo.Update(ctx, &sk); err != nil {
		return nil, err
	}
	s.publish(event.SkillUpdated, &sk)
	return &sk, nil
}

// SetStatus durumu değiştirir (etkinleştir/devre dışı bırak). Tüm kurallar yeniden denetlenir:
// eksik executor tanımlı bir taslak active yapılamaz.
func (s *Service) SetStatus(ctx context.Context, skillID string, st Status) (*Skill, error) {
	sk, err := s.repo.Get(ctx, skillID)
	if err != nil {
		return nil, err
	}
	sk.Status = st
	Normalize(sk)
	if errs := append(Validate(sk), s.checkExecutorTool(ctx, sk, nil)...); len(errs) > 0 {
		return nil, &ValidationError{Errors: errs}
	}
	sk.UpdatedAt = s.now()
	if err := s.repo.Update(ctx, sk); err != nil {
		return nil, err
	}
	s.publish(event.SkillUpdated, sk)
	return sk, nil
}

// Duplicate skill'in kopyasını taslak olarak oluşturur; adı "<ad>_copy", "<ad>_copy2", ... olur.
func (s *Service) Duplicate(ctx context.Context, skillID string) (*Skill, error) {
	src, err := s.repo.Get(ctx, skillID)
	if err != nil {
		return nil, err
	}
	name, err := s.freeCopyName(ctx, src.Name)
	if err != nil {
		return nil, err
	}
	cp := src.Clone()
	cp.ID = id.New("skill")
	cp.Name = name
	cp.Status = StatusDraft
	cp.Embedding = nil
	now := s.now()
	cp.CreatedAt, cp.UpdatedAt = now, now
	Normalize(&cp)
	if errs := Validate(&cp); len(errs) > 0 {
		return nil, &ValidationError{Errors: errs}
	}
	cp.Embedding = s.embedSkill(ctx, &cp) // ad değiştiği için kaynağın vektörü kopyalanmaz
	if err := s.repo.Create(ctx, &cp); err != nil {
		return nil, err
	}
	s.publish(event.SkillCreated, &cp)
	return &cp, nil
}

func (s *Service) freeCopyName(ctx context.Context, base string) (string, error) {
	for i := 1; i <= maxCopyAttempts; i++ {
		suffix := "_copy"
		if i > 1 {
			suffix += strconv.Itoa(i)
		}
		b := base
		if over := len(b) + len(suffix) - 64; over > 0 { // ad en fazla 64 karakter
			b = b[:len(b)-over]
		}
		name := b + suffix
		if _, err := s.repo.GetByName(ctx, name); errors.Is(err, ErrNotFound) {
			return name, nil
		} else if err != nil {
			return "", err
		}
	}
	return "", fmt.Errorf("skill: %q için boş bir kopya adı bulunamadı", base)
}

// Delete skill'i siler; yoksa ErrNotFound.
func (s *Service) Delete(ctx context.Context, skillID string) error {
	existing, err := s.repo.Get(ctx, skillID)
	if err != nil {
		return err
	}
	if err := s.repo.Delete(ctx, skillID); err != nil {
		return err
	}
	s.publish(event.SkillDeleted, existing)
	return nil
}

// Get tek skill döndürür; yoksa ErrNotFound.
func (s *Service) Get(ctx context.Context, skillID string) (*Skill, error) {
	return s.repo.Get(ctx, skillID)
}

// List filtreye uyan skill'leri ada göre sıralı döndürür.
func (s *Service) List(ctx context.Context, f ListFilter, limit, offset int) ([]Skill, error) {
	return s.repo.List(ctx, f, limit, offset)
}

// Validate kaydetmeden önce doğrular ve TÜM hataları döndürür (arayüzün canlı doğrulaması için).
// skillID doluysa (güncelleme) o skill'in kendi adı çakışma sayılmaz.
func (s *Service) Validate(ctx context.Context, skillID string, in Skill) ([]FieldError, error) {
	sk := in.Clone()
	Normalize(&sk)
	errs := s.checkExecutorTool(ctx, &sk, Validate(&sk))
	if sk.Name != "" {
		other, err := s.repo.GetByName(ctx, sk.Name)
		switch {
		case errors.Is(err, ErrNotFound):
		case err != nil:
			return nil, err
		case other.ID != skillID:
			errs = append(errs, FieldError{Field: "name", Message: "bu ada sahip bir skill zaten var"})
		}
	}
	if errs == nil {
		errs = []FieldError{}
	}
	return errs, nil
}

func (s *Service) publish(t event.Type, sk *Skill) {
	if s.bus == nil {
		return
	}
	s.bus.Publish(context.Background(), event.Event{
		Type: t,
		Data: map[string]any{"id": sk.ID, "name": sk.Name},
	})
}
