// Package settings çalışma zamanı ayarlarını yönetir: model, temperature ve üretim parametreleri.
//
// Neden ayrı paket: bu ayarlar .env ile SABİTTİR; spec ise kullanıcı arayüzünden
// değiştirilebilir olmasını istiyor (Angular Settings ekranı, /api/settings).
// Yapılandırma (config) = başlangıç değerleri, ayarlar (settings) = değişebilir durum.
//
// Depolama: şimdilik bellek içi; kalıcılık istenirse Mongo'ya taşınır
// (aynı Repository deseniyle). Değişiklik olay bus'ına yayınlanır.
package settings

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"fiko-ai/internal/event"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/prompt"
)

// Values ayar değerleridir. JSON etiketleri API sözleşmesidir (docs/API.md).
type Values struct {
	Model       string  `json:"model"`
	Temperature float64 `json:"temperature"`
	TopP        float64 `json:"topP"`
	MaxTokens   int     `json:"maxTokens"`
	ContextSize int     `json:"contextSize"`

	// ConfirmMode yazma/silme işlemlerinde kullanıcı onayının NE ZAMAN istendiğidir.
	// Bkz. ConfirmModeValues (aşağıda).
	ConfirmMode string `json:"confirmMode"`

	// PromptOverrides ve PromptDisabled yönetici tarafından düzenlenen sistem
	// istemi parçalarıdır. Boşsa kod varsayılanları kullanılır (mevcut davranış).
	//
	// Neden ayar olarak burada? İstem her tur ÜRETİLİR; ayar anında etkili olmalı
	// (sunucu yeniden başlatmaya gerek kalmasın), tıpkı temperature gibi.
	PromptOverrides map[string]string `json:"promptOverrides,omitempty"`
	PromptDisabled  map[string]bool   `json:"promptDisabled,omitempty"`
}

// Onay modları: yazma/silme aracı çalışmadan önce kullanıcı onayı istenip istenmeyeceği.
//
// Neden gerekli? Model geri alınamaz işlemleri (silme) kendi başına başlatabiliyordu.
// Canlı hata: kullanıcı "proje silme aracı yok" dedi — araç vardı ama HIGH risk +
// delete izni gerektirdiği için modele hiç gösterilmiyordu. Onay akışı bu eşiği
// düşürür: araç modele görünür, karar kullanıcının onayına bağlanır.
const (
	// ConfirmAlways: her yazma ve silme işlemi onay ister.
	ConfirmAlways = "always"
	// ConfirmRisky: yalnızca geri alınamaz işlemler (silme, arşiv, toplu silme) onay ister.
	ConfirmRisky = "risky"
	// ConfirmMissing: yalnızca eksik/şüpheli veri varsa sorar (doğrulama amaçlı).
	ConfirmMissing = "missing"
	// ConfirmNever: onay istemez, işlem doğrudan çalışır (ileri düzey kullanıcı).
	ConfirmNever = "never"
)

// ConfirmModeValues geçerli onay modları (API doğrulaması ve ayar ekranı için).
var ConfirmModeValues = []string{ConfirmAlways, ConfirmRisky, ConfirmMissing, ConfirmNever}

// DefaultConfirmMode güvenli varsayılandır: geri alınamaz işlemler mutlaka onay ister.
const DefaultConfirmMode = ConfirmRisky

// ValidConfirmMode bir modun geçerli olup olmadığını söyler (boş = geçerli sayılır,
// çünkü nil ayar "değiştirilmedi" demektir).
func ValidConfirmMode(m string) bool {
	switch m {
	case ConfirmAlways, ConfirmRisky, ConfirmMissing, ConfirmNever:
		return true
	}
	return false
}

// Sınırlar: LLM sağlayıcıları 0..1 aralığını bekler; 0 veya negatif değer anlamsızdır.
// temperature=0 deterministik, 1 çok rastgele. maxTokens/contextSize 0 = "sağlayıcı varsayılanı".
const (
	MinTemperature = 0.0
	MaxTemperature = 2.0
	MaxModelTokens = 1_000_000
)

// Service ayarları okur/yazar ve değişiklikleri yayınlar.
type Service struct {
	mu     sync.RWMutex
	values Values
	log    *slog.Logger
	bus    event.Bus // nil olabilir

	// path doluysa ayarlar bu JSON dosyasına yazılır ve açılışta buradan okunur.
	// fileMu, eşzamanlı iki Update'in dosyayı yarım/karışık yazmasını engeller.
	path   string
	fileMu sync.Mutex
}

// New Service'i başlangıç değerleriyle kurar. Geçersiz değerler düzeltilir
// (sunucu çalışırken bozuk ayarla açılıp üretim yapmamalı).
func New(v Values, log *slog.Logger, bus event.Bus) *Service {
	if log == nil {
		log = slog.Default()
	}
	s := &Service{values: v, log: log, bus: bus}
	s.normalize()
	return s
}

// normalize değerleri güvenli aralığa çeker.
func (s *Service) normalize() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.values = normalize(s.values)
}

func normalize(v Values) Values {
	if v.Temperature < MinTemperature {
		v.Temperature = MinTemperature
	}
	if v.Temperature > MaxTemperature {
		v.Temperature = MaxTemperature
	}
	if v.TopP < MinTemperature {
		v.TopP = 0
	}
	if v.TopP > 1 {
		v.TopP = 1
	}
	if v.MaxTokens < 0 {
		v.MaxTokens = 0
	}
	if v.MaxTokens > MaxModelTokens {
		v.MaxTokens = MaxModelTokens
	}
	if v.ContextSize < 0 {
		v.ContextSize = 0
	}
	// Boş/geçersiz onay modu güvenli varsayılana düşer (sunucu bozuk ayarla çalışmamalı).
	if !ValidConfirmMode(v.ConfirmMode) {
		v.ConfirmMode = DefaultConfirmMode
	}
	return v
}

// ConfirmMode anlık onay politikasını döndürür (arayüz ve tool katmanı buradan okur).
func (s *Service) ConfirmMode() string {
	m := s.Get().ConfirmMode
	if !ValidConfirmMode(m) {
		return DefaultConfirmMode
	}
	return m
}

// EnablePersistence ayarları verilen JSON dosyasına bağlar.
//
// Neden? Ayarlar yalnızca bellekteydi: kullanıcı arayüzden model seçse bile sunucu
// yeniden başladığında .env'deki OLLAMA_MODEL'e dönülüyordu ("seçtiğim model
// unutuluyor" hatası). Dosya varsa içindeki değerler .env başlangıç değerlerinin
// ÜSTÜNE biner; yoksa sessizce .env değerleriyle devam edilir ve ilk değişiklikte
// dosya oluşturulur. path boşsa kalıcılık kapalıdır.
func (s *Service) EnablePersistence(path string) error {
	path = strings.TrimSpace(path)
	if path == "" {
		return nil
	}
	s.path = path

	raw, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		return nil // ilk açılış: kayıtlı ayar yok
	}
	if err != nil {
		return fmt.Errorf("settings: %s okunamadı: %w", path, err)
	}

	var saved Values
	if err := json.Unmarshal(raw, &saved); err != nil {
		return fmt.Errorf("settings: %s bozuk: %w", path, err)
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	merged := s.values
	// Boş model kayıtlı değer sayılmaz: .env modeli korunur.
	if strings.TrimSpace(saved.Model) != "" {
		merged.Model = strings.TrimSpace(saved.Model)
	}
	// Sayısal alanlarda 0 geçerli bir seçimdir ("sağlayıcı varsayılanı"), bu yüzden
	// dosyadaki değer olduğu gibi alınır.
	merged.Temperature = saved.Temperature
	merged.TopP = saved.TopP
	merged.MaxTokens = saved.MaxTokens
	merged.ContextSize = saved.ContextSize
	if ValidConfirmMode(saved.ConfirmMode) {
		merged.ConfirmMode = saved.ConfirmMode
	}
	if saved.PromptOverrides != nil {
		merged.PromptOverrides = saved.PromptOverrides
	}
	if saved.PromptDisabled != nil {
		merged.PromptDisabled = saved.PromptDisabled
	}
	s.values = normalize(merged)
	s.log.Info("kayıtlı ayarlar yüklendi", "path", path, "model", s.values.Model)
	return nil
}

// persist güncel ayarları dosyaya yazar (geçici dosya + yeniden adlandırma: yarım dosya kalmaz).
// Hata Update'i BAŞARISIZ yapmaz: ayar bellekte geçerlidir, yalnızca uyarı loglanır.
func (s *Service) persist() {
	if s.path == "" {
		return
	}
	s.fileMu.Lock()
	defer s.fileMu.Unlock()

	data, err := json.MarshalIndent(s.Get(), "", "  ")
	if err != nil {
		s.log.Warn("ayarlar kaydedilemedi", "err", err)
		return
	}
	if dir := filepath.Dir(s.path); dir != "." {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			s.log.Warn("ayar klasörü oluşturulamadı", "dir", dir, "err", err)
			return
		}
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o600); err != nil {
		s.log.Warn("ayarlar kaydedilemedi", "path", s.path, "err", err)
		return
	}
	if err := os.Rename(tmp, s.path); err != nil {
		s.log.Warn("ayarlar kaydedilemedi", "path", s.path, "err", err)
		_ = os.Remove(tmp)
	}
}

// Get anlık değerleri döndürür (kopya; çağıran değiştiremez).
func (s *Service) Get() Values {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.values
}

// LLMOptions agent'in sağlayıcıya göndereceği parametreleri üretir.
// Sıfır değerler "sağlayıcı varsayılanı" demektir; temperature/topP işaretçidir çünkü
// 0 geçerli bir değerdir ve "belirtilmemiş" ile karıştırılmamalıdır.
func (s *Service) LLMOptions() llm.Options {
	v := s.Get()
	opts := llm.Options{MaxTokens: v.MaxTokens, ContextSize: v.ContextSize}
	if v.Temperature > 0 {
		t := v.Temperature
		opts.Temperature = &t
	}
	// topP 0 = "belirtilmedi" (sağlayıcı varsayılanı); >0 ise sağlayıcıya iletilir.
	if v.TopP > 0 {
		p := v.TopP
		opts.TopP = &p
	}
	return opts
}

// Model adını döndürür (ws'in text.message payload'ında kullanılır).
func (s *Service) Model() string { return s.Get().Model }

// PromptOverrides, yöneticinin özel istem metinlerini döndürür (kod varsayılanı için nil).
func (s *Service) PromptOverrides() prompt.Overrides { return s.Get().PromptOverrides }

// PromptDisabled, yöneticinin KAPATTIĞI istem parçalarını döndürür.
func (s *Service) PromptDisabled() prompt.Disabled { return s.Get().PromptDisabled }

// Patch kısmi güncellemedir: nil alan "değiştirme" demektir. 0 değeri AÇIKÇA verilebilir
// ("sağlayıcı varsayılanı"na / deterministik moda dönmek için); Values üzerinden bu mümkün değildir
// çünkü orada sıfır "verilmedi" ile karışır. JSON etiketleri API sözleşmesidir (PUT /api/settings).
type Patch struct {
	Model       *string  `json:"model,omitempty"`
	Temperature *float64 `json:"temperature,omitempty"`
	TopP        *float64 `json:"topP,omitempty"`
	MaxTokens   *int     `json:"maxTokens,omitempty"`
	ContextSize *int     `json:"contextSize,omitempty"`
	ConfirmMode *string  `json:"confirmMode,omitempty"`
	// PromptOverrides/Disabled: yönetici düzenlemeleri. Sözlük olduğu için "temizlemek"
	// de mümkündür: boş nesne göndermek overrides'ı sıfırlar (kod varsayılanına dönüş).
	PromptOverrides map[string]string `json:"promptOverrides,omitempty"`
	PromptDisabled  map[string]bool   `json:"promptDisabled,omitempty"`
}

// Set sıfır olmayan alanları günceller (sıfır/boş = değiştirme). Başlangıç yapılandırması için
// uygundur; sıfıra dönmek gerekiyorsa Update kullanılır.
func (s *Service) Set(patch Values) (Values, error) {
	var p Patch
	if patch.Model != "" {
		p.Model = &patch.Model
	}
	if patch.Temperature != 0 {
		p.Temperature = &patch.Temperature
	}
	if patch.TopP != 0 {
		p.TopP = &patch.TopP
	}
	if patch.MaxTokens != 0 {
		p.MaxTokens = &patch.MaxTokens
	}
	if patch.ContextSize != 0 {
		p.ContextSize = &patch.ContextSize
	}
	return s.Update(p)
}

// Update verilen (nil olmayan) alanları günceller; 0 açıkça verilebilir. Değişiklik bus'a yayınlanır.
// Geçersiz değerde hiçbir şey değişmez ve hata döner.
func (s *Service) Update(patch Patch) (Values, error) {
	s.mu.Lock()
	before := s.values
	next := before

	if patch.Model != nil {
		if strings.TrimSpace(*patch.Model) == "" {
			s.mu.Unlock()
			return before, fmt.Errorf("settings: model boş olamaz")
		}
		next.Model = strings.TrimSpace(*patch.Model)
	}
	if patch.Temperature != nil {
		next.Temperature = *patch.Temperature
	}
	if patch.TopP != nil {
		next.TopP = *patch.TopP
	}
	if patch.MaxTokens != nil {
		next.MaxTokens = *patch.MaxTokens
	}
	if patch.ContextSize != nil {
		next.ContextSize = *patch.ContextSize
	}
	if patch.ConfirmMode != nil {
		mode := strings.TrimSpace(*patch.ConfirmMode)
		if !ValidConfirmMode(mode) {
			s.mu.Unlock()
			return before, fmt.Errorf("settings: confirmMode geçersiz (always|risky|missing|never olmalı)")
		}
		next.ConfirmMode = mode
	}
	// İstem parçaları: istemci doğrudan koda metin sokabildiği için katalogla
	// sınırlanır (bkz. prompt.CleanOverrides / prompt.CleanDisabled).
	if patch.PromptOverrides != nil {
		ov, err := prompt.CleanOverrides(prompt.Overrides(patch.PromptOverrides))
		if err != nil {
			s.mu.Unlock()
			return before, err
		}
		next.PromptOverrides = ov
	}
	if patch.PromptDisabled != nil {
		next.PromptDisabled = prompt.CleanDisabled(prompt.Disabled(patch.PromptDisabled))
	}

	if next.Temperature > MaxTemperature {
		s.mu.Unlock()
		return before, fmt.Errorf("settings: temperature en fazla %v olabilir", MaxTemperature)
	}
	if next.Temperature < MinTemperature {
		s.mu.Unlock()
		return before, fmt.Errorf("settings: temperature negatif olamaz")
	}
	if next.TopP > 1 || next.TopP < 0 {
		s.mu.Unlock()
		return before, fmt.Errorf("settings: topP 0..1 aralığında olmalı")
	}
	if next.ContextSize < 0 || next.ContextSize > MaxModelTokens {
		s.mu.Unlock()
		return before, fmt.Errorf("settings: contextSize 0..%d aralığında olmalı", MaxModelTokens)
	}
	if next.MaxTokens < 0 || next.MaxTokens > MaxModelTokens {
		s.mu.Unlock()
		return before, fmt.Errorf("settings: maxTokens 0..%d aralığında olmalı", MaxModelTokens)
	}

	s.values = next
	s.mu.Unlock()

	// Seçilen model/ayarlar yeniden başlatmada kaybolmasın.
	s.persist()

	s.log.Info("ayarlar güncellendi",
		"model", next.Model, "temperature", next.Temperature, "topP", next.TopP,
		"maxTokens", next.MaxTokens, "contextSize", next.ContextSize,
		"confirmMode", next.ConfirmMode)

	if s.bus != nil {
		s.bus.Publish(context.Background(), event.Event{
			Type: event.SettingsChanged,
			Data: map[string]any{
				"model":       next.Model,
				"temperature": next.Temperature,
				"topP":        next.TopP,
			},
		})
	}
	return next, nil
}
