// Package config ortam değişkenlerinden (ve varsa .env dosyasından) yapılandırmayı yükler.
// Hiçbir sır kaynak koda yazılmaz; her şey ortamdan gelir.
package config

import (
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/joho/godotenv"
)

type Config struct {
	Env string
	// SettingsPath çalışma zamanı ayarlarının (model, temperature...) kalıcı JSON dosyasıdır.
	// Boşsa ayarlar yalnızca bellekte tutulur ve yeniden başlatmada .env değerlerine döner.
	SettingsPath string
	HTTP         HTTP
	Mongo        Mongo
	Ollama       Ollama
	LLM          LLM
	Auth         Auth
	Users        Users
	Chat         Chat
	WS           WS
	Log          Log
	Audit        Audit
	Tools        Tools
	STT          STT
	TTS          TTS
	// Knowledge bilgi tabanı (RAG, Faz 8) ayarlarıdır.
	Knowledge Knowledge
	// Workflow akış motoru (Faz 9) ayarlarıdır.
	Workflow Workflow
	// Memory kalıcı bellek (Faz 10) ayarlarıdır.
	Memory Memory
	// Legacy eski sistemlerin özellik bayraklarıdır (docs/GOREV-HAFIZA.md, Faz 1).
	Legacy Legacy
	// Claims Iddia Hafızası (docs/GOREV-HAFIZA.md) ayarlarıdır.
	Claims Claims
}

// Claims, Iddia Hafızası'nın sohbetten yazma ayarlarıdır.
type Claims struct {
	// Write açıkken sohbet mesajlarındaki bilgiler ekip ortak `claims` deposuna yazılır.
	// FIKO_CLAIMS_WRITE. Varsayılan KAPALI: yazma yalnızca gerçek oturum kimliğiyle
	// (FIKO_USERS_ENABLED=true) çalışır; üyelik kapalıyken açık olsa bile hiçbir şey yazılmaz.
	Write bool
	// Read açıkken her kullanıcı mesajı için ekip hafızası okunur: liste soruları sunucuda tam liste
	// olarak üretilir, bilgi soruları en yakın kayıtlarla cevaplanır (Faz 4). FIKO_CLAIMS_READ.
	// Varsayılan KAPALI; yazma gibi yalnızca gerçek oturum kimliğiyle çalışır. Her mesaja ek bir
	// sınıflandırma LLM çağrısı ve bir embedding çağrısı ekler (turun başlangıcını geciktirmez:
	// bellek/bilgi tabanı aramasıyla paralel yürür, ama ilk kelimeden önce BİTMESİ beklenir).
	Read bool
}

// Legacy, Iddia Hafızası'na geçişte DEAKTİF edilen eski sistemlerin bayraklarıdır.
//
// Kod SİLİNMEZ: bayrak false iken eski sistem kurulmaz, route'ları kaydedilmez ve
// arayüz menüsü gizlenir; true yapılınca eskisi gibi çalışır.
type Legacy struct {
	// Company şirket bölümünü (biz.* tool'ları ve verisi) açar. FIKO_LEGACY_COMPANY.
	Company bool
	// Tools eski tool sistemini açar: ajanın tool çağırması, /api/tools ve
	// /api/skills/:id/execute uçları ile arayüzdeki Tools menüsü. FIKO_LEGACY_TOOLS.
	Tools bool
}

// Workflow akış motoru (Faz 9) ayarlarıdır.
type Workflow struct {
	// MaxConcurrent aynı anda çalışan yürütme sayısıdır. Sınıra ulaşıldığında
	// POST /run "429" döner: kullanıcı akışın çalıştığını görür, kuyruğa girmez.
	// 0 = workflow.DefaultMaxConcurrent.
	MaxConcurrent int
	// RunTimeout tek bir yürütmenin üst süresidir; aşan yürütme "failed" olur.
	// 0 = workflow.DefaultRunTimeout (5 dakika).
	RunTimeout time.Duration
}

// Knowledge bilgi tabanı ayarlarıdır.
type Knowledge struct {
	// VectorSearch MongoDB $vectorSearch kullanımı: "auto" (varsayılan: mongot varsa kullan, yoksa
	// Go tarafı skorlama), "on" (kullanılamıyorsa UYARI ver; yine de Go tarafına düşer), "off" (hiç deneme).
	// $vectorSearch mongot gerektirir (Atlas ya da MongoDB 8.2+ community + mongot).
	VectorSearch string
	// VectorIndexTimeout vektör index'inin sorgulanabilir olmasını bekleme süresi (ilk kurulum dakikalar sürebilir).
	// Bekleme arka plandadır, sunucu açılışını geciktirmez; bu sürede arama Go tarafı skorlamayla çalışır.
	VectorIndexTimeout time.Duration
}

type HTTP struct {
	Addr            string
	AllowedOrigins  []string
	ShutdownTimeout time.Duration

	// TLSCertFile / TLSKeyFile: ikisi de verilirse sunucu HTTPS konuşur.
	// Boşsa HTTP (geliştirme varsayılanı).
	//
	// Neden gerekli? Arayüz ve API aynı origin'de yayınlandığında (fikoyt.com:4200)
	// tarayıcı https:// sayfadan http:// API'ye giden isteği "mixed content"
	// olarak engeller. Bu yüzden API de aynı portta TLS konuşmalıdır.
	TLSCertFile string
	TLSKeyFile  string

	// StaticDir: üretilen arayüzün (Angular build) sunulacağı dizin.
	// Boşsa yalnız API yayınlanır. Ayrı web sunucusu kurmadan tek origin yayın için.
	StaticDir string
}

// LLM üretim parametrelerinin başlangıç değerleridir.
// Kullanıcı arayüzünden değiştirilebilir (/api/settings); .env yalnızca başlangıç değerlerini verir.
type LLM struct {
	Temperature float64
	TopP        float64
	MaxTokens   int
	ContextSize int
}

type Mongo struct {
	URI            string
	Database       string
	ConnectTimeout time.Duration
}

type Ollama struct {
	BaseURL        string
	Model          string
	EmbedModel     string        // boşsa embedding kapalı (skill araması anahtar kelimeyle çalışır)
	RequestTimeout time.Duration // Streaming olmayan istekler ve header bekleme süresi

	// KeepAlive, üretim bitince modelin VRAM'de kalacağı süredir.
	// Neden? Ollama varsayılanında modeli ~5 dk sonra boşaltır; sohbet arasında
	// kalan her üretimde model YENİDEN yüklenir (20-60 sn bekleme, gerçek zaman hissini
	// öldürür). "30m" gibi bir değer modeli bellekte tutar.
	// Boş bırakılırsa keep_alive GÖNDERİLMEZ (Ollama varsayılanı geçerli olsun).
	KeepAlive string

	// MaxContext, istemci num_ctx GÖNDERMEDİĞİNDE uygulanan tavantır.
	// Neden? Ollama num_ctx verilmezse model kartındaki değeri kullanır; büyük modellerde
	// bu 131072 gibi devasa değerler olur ve KV cache için onlarca GB VRAM ayırır.
	// Bağlam yöneticisi (ctxmgr) zaten gönderilecek mesajları ~4k tokena kırptığı için
	// devasa pencere hiç kullanılmaz, sadece bellek yer. 0 = tavan yok (kullanıcı isterse).
	MaxContext int

	// NumGPU: Ollama'nın kaç katmanı GPU'ya yükleyeceği (99 = hepsi, 0 = otomatik).
	// NumBatch: GPU'ya gönderilen token yığını boyutu (büyük = daha hızlı prefill).
	// NumThread: CPU iş parçacığı (0 = otomatik).
	// 0 değerleri GÖNDERİLMEZ; sağlayıcı/Ollama varsayılanı geçerli olsun.
	NumGPU    int
	NumBatch  int
	NumThread int

	// MaxAttempts, sağlayıcıya yapılacak TOPLAM deneme sayısıdır (1 = tekrar yok).
	//
	// Neden gerekiyor? Cloud modellerde (`:cloud` soneki) Ollama isteği
	// ollama.com'a PROXY'ler ve upstream bağlantısı arada 502 / PROTOCOL_ERROR
	// ile kopar. Bu GEÇİCİ bir hatadır: aynı istek birkaç saniye sonra 200 döner.
	// Tek denemede kullanıcının turu tümden ölür ve sohbette "HATA" balonu kalır.
	// Retry YALNIZCA HTTP başlıkları gelmeden önce uygulanır; akış başladıktan
	// sonra asla tekrar denenmez (içerik kullanıcıya iki kez akmasın).
	MaxAttempts int

	// RetryBase, ilk tekrar arasındaki beklemedir; sonrakiler ikiye katlanır.
	// 0 = paket varsayılanı (300ms).
	RetryBase time.Duration
}

// Auth, istek yetkilendirmesi ayarlaridir.
//
// Tek paylasilan anahtar (API key) modeli kullanilir. Kullanici bazli oturum,
// rol ve yetki hiyerarsisi KAPSAM DISIDIR.
type Auth struct {
	// APIKey bosken dogrulama YAPILMAZ (yerel gelistirme akisi korunur ve
	// sistem ACIK kabul edilir). Doluyken korumali uclar X-Fiko-Auth basligi ister.
	APIKey string
}

// Users, üyelik (çok kullanıcılı) sistem ayarlarıdır.
type Users struct {
	// Enabled true ise üyelik AÇIKTIR: giriş/üyelik uçları çalışır ve istekler
	// oturum token'ı ile yapılır. false ise geliştirme akışı (X-Fiko-User) korunur.
	Enabled bool
	// BootstrapUser/Password, İLK KURULUM yöneticisidir. Password BOŞSA hesap
	// oluşturulmaz — rastgele parola üretmek kullanıcıyı kilitlerdi.
	BootstrapUser     string
	BootstrapPassword string
	// SessionTTL oturum ömrü (örn. "720h"); boşsa 30 gün.
	SessionTTL string
}

type Chat struct {
	DefaultUserID      string
	MaxHistoryMessages int
	GenerationTimeout  time.Duration
	Timezone           string
	MaxInputRunes      int
	// DefaultContextTokens LLM_CONTEXT_SIZE/ayar verilmediğinde (0 = sağlayıcı varsayılanı)
	// bağlam yöneticisinin varsaydığı pencere (token). Phase 2.
	DefaultContextTokens int
	// MaxAgentIterations tool çağırma döngüsünün üst sınırıdır (Faz 5; 0 = ajan varsayılanı).
	MaxAgentIterations int
}

type WS struct {
	PingInterval    time.Duration
	PongWait        time.Duration
	WriteWait       time.Duration
	MaxMessageBytes int64
	OutboundBuffer  int
}

type Log struct {
	Level  string // debug|info|warn|error
	Format string // text|json
}

// Audit denetim kaydı ayarlarıdır.
// Path boşsa kayıtlar stdout'a (JSON satırı) yazılır — konteyner ortamı için uygundur.
type Audit struct {
	Path string
}

// Tools, tool sistemi (Faz 4) ayarlarıdır.
// Güvenlik varsayılanları: yalnızca execute izni, yüksek risk kapalı, süreç çalıştırma kapalı.
type Tools struct {
	DefaultPermissions []string      // boşsa "execute" (güvenli varsayılan)
	AllowHighRisk      bool          // HIGH risk tool'lara izin verilsin mi
	AllowCritical      bool          // CRITICAL risk tool'lara izin verilsin mi
	MaxWait            time.Duration // bir tool'un çalışma süresi üst sınırı
	MaxAttempts        int           // en fazla deneme (0 = tool politikası)
	FSRoot             string        // dosya tool'larının kökü (sandbox)
	MaxReadBytes       int64
	MaxWriteBytes      int64
	ExecEnabled        bool     // sys.exec'i AÇ (izin listesi boşsa etkisiz)
	ExecAllowed        []string // çalıştırılabilir programların tam yolları
	ExecTimeout        time.Duration
	// HTTPEnabled uzak (kind=http) tool'lar için genel kapıdır; false ise çalışmazlar.
	HTTPEnabled bool
	// HTTPAllowPrivate true ise uzak tool'lar loopback/özel ağa ulaşabilir.
	// SSRF korumasını kapatır: YALNIZCA yerel CATIA otomasyon servisi gibi
	// bilerek hedeflenen senaryolarda açılmalı, üretimde kapalı kalmalı.
	HTTPAllowPrivate bool
	HTTPMaxBytes     int64
	HTTPMaxRedirects int
	HTTPTimeout      time.Duration

	// WebEnabled internet tool'larını (web.fetch, web.search, web.exchange_rate) etkinleştirir.
	// HTTPEnabled=false iken etkisizdir. Tool'lar yine de "read" izni ister.
	WebEnabled bool
	// WebSearchProvider: duckduckgo (anahtarsız, varsayılan) | searxng | brave.
	WebSearchProvider string
	WebSearchURL      string // searxng örnek adresi
	WebSearchKey      string // brave API anahtarı (yalnızca ortamdan; koda yazılmaz)

	// BizEnabled şirket yönetimi tool'larını (biz.*) etkinleştirir; BizPath verinin JSON dosyasıdır.
	BizEnabled bool
	BizPath    string
}

// STT konuşma tanıma ayarlarıdır (Phase 6). URL boşsa sesli giriş KAPALI'dır:
// sunucu voice.start isteğine "yapılandırılmamış" der. Motor seçimi KULLANICININ
// kararıdır; iki protokol sağlanır: "whisper.cpp" (yerel) ve "openai" (bulut/uyumlu).
type STT struct {
	Enabled  bool
	Provider string // whispercpp | openai
	URL      string
	APIKey   string // yalnızca openai sağlayıcısında
	Model    string // yalnızca openai sağlayıcısında
	Language string
	Timeout  time.Duration
	// MaxSeconds ve MaxBytes kayıt sınırlarıdır (bkz. stt.Limits).
	MaxSeconds int
	MaxBytes   int64
}

// TTS ses üretimi ayarlarıdır (Phase 7). URL boşsa sesli çıkış KAPALI'dır:
// istemci tts.mode gönderebilir ama sunucu asistan yanıtlarını seslendirmez.
// Sağlanan protokoller: "openai" (OpenAI-uyumlu /v1/audio/speech) ve "piper" (yerel HTTP).
type TTS struct {
	Enabled  bool
	Provider string // openai | piper
	URL      string
	APIKey   string
	Model    string
	Voice    string
	Format   string
	Speed    float64
	Timeout  time.Duration
	MaxChars int // tek cümle çağrısının karakter sınırı
}

// Load .env dosyasını (varsa) okur, sonra ortam değişkenlerinden Config üretir.
func Load() (Config, error) {
	// .env isteğe bağlıdır; backend/ veya proje kökünden çalıştırılabilir.
	for _, p := range []string{".env", "backend/.env", "../.env"} {
		if _, err := os.Stat(p); err == nil {
			_ = godotenv.Load(p)
			break
		}
	}

	cfg := Config{
		Env:          getString("APP_ENV", "development"),
		SettingsPath: getString("SETTINGS_PATH", filepath.Join("data", "settings.json")),
		HTTP: HTTP{
			Addr:            getString("HTTP_ADDR", ":8080"),
			AllowedOrigins:  splitList(getString("HTTP_ALLOWED_ORIGINS", "http://localhost:4200,http://127.0.0.1:4200")),
			ShutdownTimeout: getDuration("HTTP_SHUTDOWN_TIMEOUT", 15*time.Second),
			TLSCertFile:     getString("HTTP_TLS_CERT", ""),
			TLSKeyFile:      getString("HTTP_TLS_KEY", ""),
			StaticDir:       getString("HTTP_STATIC_DIR", ""),
		},
		Mongo: Mongo{
			URI:            getString("MONGO_URI", "mongodb://localhost:27017"),
			Database:       getString("MONGO_DATABASE", "fiko_ai"),
			ConnectTimeout: getDuration("MONGO_CONNECT_TIMEOUT", 10*time.Second),
		},
		Ollama: Ollama{
			BaseURL:        strings.TrimRight(getString("OLLAMA_BASE_URL", "http://localhost:11434"), "/"),
			Model:          getString("OLLAMA_MODEL", ""),
			EmbedModel:     getString("OLLAMA_EMBED_MODEL", ""),
			RequestTimeout: getDuration("OLLAMA_REQUEST_TIMEOUT", 30*time.Second),
			KeepAlive:      getString("OLLAMA_KEEP_ALIVE", "30m"),
			MaxContext: getInt("OLLAMA_MAX_CONTEXT", 131072),
			NumGPU:         getInt("OLLAMA_NUM_GPU", 0),
			NumBatch:       getInt("OLLAMA_NUM_BATCH", 0),
			NumThread:      getInt("OLLAMA_NUM_THREAD", 0),
			MaxAttempts:    getInt("OLLAMA_MAX_ATTEMPTS", 3),
			RetryBase:      getDuration("OLLAMA_RETRY_BASE_DELAY", 300*time.Millisecond),
		},
		// Auth: API anahtari. BOSSA dogrulama yapilmaz (yerel gelistirme).
		// DOLUYSA korumali uclar X-Fiko-Auth basligini zorunlu tutar.
		Auth: Auth{
			APIKey: getString("FIKO_API_KEY", ""),
		},
		Users: Users{
			Enabled:           getBool("FIKO_USERS_ENABLED", false),
			BootstrapUser:     getString("FIKO_BOOTSTRAP_USER", "admin"),
			BootstrapPassword: getString("FIKO_BOOTSTRAP_PASSWORD", ""),
			SessionTTL:        getString("FIKO_SESSION_TTL", ""),
		},
		Chat: Chat{
			DefaultUserID:        getString("DEFAULT_USER_ID", "local"),
			MaxHistoryMessages:   getInt("CHAT_MAX_HISTORY_MESSAGES", 60),
			GenerationTimeout:    getDuration("CHAT_GENERATION_TIMEOUT", 5*time.Minute),
			Timezone:             getString("CHAT_TIMEZONE", "Europe/Istanbul"),
			MaxInputRunes:        getInt("CHAT_MAX_INPUT_RUNES", 8000),
			DefaultContextTokens: getInt("CHAT_DEFAULT_CONTEXT_TOKENS", 131072),
			MaxAgentIterations:   getInt("CHAT_MAX_AGENT_ITERATIONS", 4),
		},
		LLM: LLM{
			// 0 = "sağlayıcı varsayılanı" (LLM parametresi hiç gönderilmez).
			// Bu yüzden varsayılan 0'dır; kullanıcı arayüzünden ayarlanır.
			Temperature: getFloat("LLM_TEMPERATURE", 0),
			TopP:        getFloat("LLM_TOP_P", 0),
			MaxTokens:   getInt("LLM_MAX_TOKENS", 0),
			ContextSize: getInt("LLM_CONTEXT_SIZE", 0),
		},
		Audit: Audit{
			// Boşsa stdout; dosya verilirse append modunda açılır (0600).
			Path: getString("AUDIT_LOG_PATH", ""),
		},
		Tools: Tools{
			// Varsayılan izinler: read + write + execute.
			//
			// Neden değişti? Canlı hata (04.10.2026): "proje silme aracı yok" deniyordu ve
			// sebep izinlerdi: yalnızca "execute" verildiği için read/write/delete isteyen
			// ŞİRKET tool'larının 6 tanesi (biz.add, biz.find, biz.summary, biz.update,
			// biz.delete, biz.project) modele HİÇ SUNULAMIYORDU. Yani araç kodda vardı,
			// hatta kullanıcı izin ekranından açılabiliyordu — ama varsayılan olarak
			// görünmezdi.
			//
			// Güvenlik neden bozulmuyor:
			//   - delete YALNIZCA biz.delete (ve fs.delete) gibi onay FORMU olan araçları
			//     açar; form üretimi + kullanıcı onayı aracın içindedir, model tek başına
			//     silemez (bkz. builtin/confirm.go, agent/confirm.go).
			//   - TOOL_ALLOW_HIGH_RISK / _CRITICAL false kalır: kritik araçlar (sys.exec)
			//     modele hiç sunulmaz.
			//   - write yalnızca uygulamanın kendi verisine yazar (biz.* kayıtları,
			//     kalıcı bellek); dosya sistemi tool'ları (fs.write_text) kendi
			//     TOOL_FS_ROOT sandbox'ına yazmaya devam eder.
			//   - system YOK: süreç çalıştırma (sys.exec) ayrıca TOOL_EXEC_ENABLED=false
			//     ile kapalıdır ve izin listesinde tam yol gerektirir.
			//
			// Neden delete de eklendi? (Canlı hata 04.10.2026) delete izni olmadığı için
			// biz.delete modele hiç sunulmuyordu; kullanıcı "projemi sil" dediğinde araç
			// yok deniyordu. Silme yine de güvenlidir: araç confirmed gelmeden silmez.
			DefaultPermissions: splitList(getString("TOOL_DEFAULT_PERMISSIONS", "read,write,execute,delete")),
			AllowHighRisk:      getBool("TOOL_ALLOW_HIGH_RISK", false),
			AllowCritical:      getBool("TOOL_ALLOW_CRITICAL", false),
			MaxWait:            getDuration("TOOL_MAX_WAIT", 2*time.Minute),
			MaxAttempts:        getInt("TOOL_MAX_ATTEMPTS", 0),
			FSRoot:             getString("TOOL_FS_ROOT", filepath.Join("data", "tools")),
			MaxReadBytes:       int64(getInt("TOOL_MAX_READ_BYTES", 1<<20)),
			MaxWriteBytes:      int64(getInt("TOOL_MAX_WRITE_BYTES", 1<<20)),
			ExecEnabled:        getBool("TOOL_EXEC_ENABLED", false),
			ExecAllowed:        splitList(getString("TOOL_EXEC_ALLOW", "")),
			ExecTimeout:        getDuration("TOOL_EXEC_TIMEOUT", 60*time.Second),
			// Uzak (kind=http) tool'lar: genel kapı + SSRF politikası.
			// HTTPEnabled=false ise uzak tool'lar tanımlı olur ama ÇALIŞMAZ.
			HTTPEnabled:      getBool("TOOL_HTTP_ENABLED", true),
			HTTPAllowPrivate: getBool("TOOL_HTTP_ALLOW_PRIVATE", false),
			HTTPMaxBytes:     int64(getInt("TOOL_HTTP_MAX_BYTES", 1<<20)),
			HTTPMaxRedirects: getInt("TOOL_HTTP_MAX_REDIRECTS", 2),
			HTTPTimeout:      getDuration("TOOL_HTTP_TIMEOUT", 30*time.Second),

			// Internet tool'ları (web.*) ve şirket yönetimi tool'ları (biz.*).
			WebEnabled:        getBool("WEB_ENABLED", true),
			WebSearchProvider: getString("WEB_SEARCH_PROVIDER", "duckduckgo"),
			WebSearchURL:      getString("WEB_SEARCH_URL", ""),
			WebSearchKey:      getString("WEB_SEARCH_KEY", ""),
			BizEnabled:        getBool("BIZ_ENABLED", true),
			BizPath:           getString("BIZ_DATA_PATH", filepath.Join("data", "biz", "records.json")),
		},
		STT: STT{
			// STT_URL boşsa sesli giriş kapalıdır. İki motor seçeneği:
			//   1) whisper.cpp (yerel, gizlilik korumalı):
			//      ./server -m models/ggml-base.bin --port 9000 --host 127.0.0.1
			//      STT_PROVIDER=whispercpp
			//      STT_URL=http://127.0.0.1:9000/inference
			//   2) OpenAI-uyumlu (bulut: OpenAI/Groq/Together/Azure, ya da faster-whisper sunucusu):
			//      STT_PROVIDER=openai
			//      STT_URL=https://api.openai.com/v1/audio/transcriptions
			//      STT_API_KEY=sk-...   STT_MODEL=whisper-1
			Enabled:  getBool("STT_ENABLED", getString("STT_URL", "") != ""),
			Provider: getString("STT_PROVIDER", "whispercpp"),
			URL:      getString("STT_URL", ""),
			APIKey:   getString("STT_API_KEY", ""),
			Model:    getString("STT_MODEL", ""),
			Language: getString("STT_LANGUAGE", "tr"),
			Timeout:  getDuration("STT_TIMEOUT", 60*time.Second),

			MaxSeconds: getInt("STT_MAX_SECONDS", 30),
			MaxBytes:   int64(getInt("STT_MAX_BYTES", 8<<20)),
		},
		TTS: TTS{
			// TTS_URL boşsa sesli ÇIKTI kapalıdır (metin akışı normal çalışır).
			// Sağlanan iki protokol: openai (uyumlu uç) ve piper (yerel HTTP).
			//   TTS_PROVIDER=openai  TTS_URL=http://127.0.0.1:8080/v1/audio/speech
			//   TTS_PROVIDER=piper   TTS_URL=http://127.0.0.1:5000/api/tts
			Enabled:  getBool("TTS_ENABLED", getString("TTS_URL", "") != ""),
			Provider: getString("TTS_PROVIDER", "openai"),
			URL:      getString("TTS_URL", ""),
			APIKey:   getString("TTS_API_KEY", ""),
			Model:    getString("TTS_MODEL", ""),
			Voice:    getString("TTS_VOICE", ""),
			Format:   getString("TTS_FORMAT", ""),
			Speed:    getFloat("TTS_SPEED", 0),
			Timeout:  getDuration("TTS_TIMEOUT", 30*time.Second),
			MaxChars: getInt("TTS_MAX_CHARS", 500),
		},
		Knowledge: Knowledge{
			// auto | on | off. $vectorSearch mongot gerektirir; yoksa "auto" sessizce Go tarafı skorlamaya düşer.
			VectorSearch:       strings.ToLower(getString("KNOWLEDGE_VECTOR_SEARCH", "auto")),
			VectorIndexTimeout: getDuration("KNOWLEDGE_VECTOR_INDEX_TIMEOUT", 10*time.Minute),
		},
		// Akış motoru (Faz 9). Ayarlanmazsa paket varsayılanları geçerli olur:
		// 4 eşzamanlı yürütme, yürütme başına 5 dakika.
		Workflow: Workflow{
			MaxConcurrent: getInt("WORKFLOW_MAX_CONCURRENT", 0),
			RunTimeout:    getDuration("WORKFLOW_RUN_TIMEOUT", 0),
		},
		Memory: Memory{
			Enabled:        getBool("MEMORY_ENABLED", true),
			AutoLearn:      getBool("MEMORY_AUTO_LEARN", true),
			MaxPerUser:     getInt("MEMORY_MAX_PER_USER", 0),
			RecallMinScore: getFloat("MEMORY_RECALL_MIN_SCORE", 0.35),
			LearnModel:     getString("MEMORY_LEARN_MODEL", ""),
		},
		// Eski sistemler VARSAYILAN KAPALI (Iddia Hafızası geçişi, Faz 1).
		// Eskisini geri açmak için .env'de FIKO_LEGACY_COMPANY=true / FIKO_LEGACY_TOOLS=true.
		Legacy: Legacy{
			Company: getBool("FIKO_LEGACY_COMPANY", false),
			Tools:   getBool("FIKO_LEGACY_TOOLS", false),
		},
		Claims: Claims{Write: getBool("FIKO_CLAIMS_WRITE", false), Read: getBool("FIKO_CLAIMS_READ", false)},
		WS: WS{
			PingInterval:    getDuration("WS_PING_INTERVAL", 20*time.Second),
			PongWait:        getDuration("WS_PONG_WAIT", 50*time.Second),
			WriteWait:       getDuration("WS_WRITE_WAIT", 10*time.Second),
			MaxMessageBytes: int64(getInt("WS_MAX_MESSAGE_BYTES", 1<<20)),
			OutboundBuffer:  getInt("WS_OUTBOUND_BUFFER", 256),
		},
		Log: Log{
			Level:  getString("LOG_LEVEL", "info"),
			Format: getString("LOG_FORMAT", "text"),
		},
	}
	return cfg, cfg.Validate()
}

// Validate temel tutarlılık kontrollerini yapar.
func (c Config) Validate() error {
	if _, err := url.ParseRequestURI(c.Ollama.BaseURL); err != nil {
		return fmt.Errorf("config: OLLAMA_BASE_URL geçersiz: %w", err)
	}
	if c.Mongo.URI == "" || c.Mongo.Database == "" {
		return fmt.Errorf("config: MONGO_URI ve MONGO_DATABASE zorunlu")
	}
	if c.Chat.MaxHistoryMessages < 1 {
		return fmt.Errorf("config: CHAT_MAX_HISTORY_MESSAGES en az 1 olmalı")
	}
	// TLS: iki dosya BİRLİKTE verilmelidir. Yarım tanım sessizce HTTP'ye düşer
	// ve kullanıcı "sunucu başladı ama https yok" diye kırk dakika uğraşır.
	if (c.HTTP.TLSCertFile == "") != (c.HTTP.TLSKeyFile == "") {
		return fmt.Errorf("config: HTTP_TLS_CERT ve HTTP_TLS_KEY birlikte verilmeli (ikisi de boş = HTTP, ikisi de dolu = HTTPS)")
	}
	if c.Chat.DefaultContextTokens < 512 {
		return fmt.Errorf("config: CHAT_DEFAULT_CONTEXT_TOKENS en az 512 olmalı")
	}
	if c.WS.PongWait <= c.WS.PingInterval {
		return fmt.Errorf("config: WS_PONG_WAIT, WS_PING_INTERVAL değerinden büyük olmalı")
	}
	if c.WS.OutboundBuffer < 1 {
		return fmt.Errorf("config: WS_OUTBOUND_BUFFER en az 1 olmalı")
	}
	// 0 = "sağlayıcı varsayılanı" olduğu için yalnızca üst sınır denetlenir.
	if c.LLM.Temperature < 0 || c.LLM.Temperature > 2 {
		return fmt.Errorf("config: LLM_TEMPERATURE 0..2 aralığında olmalı")
	}
	if c.LLM.TopP < 0 || c.LLM.TopP > 1 {
		return fmt.Errorf("config: LLM_TOP_P 0..1 aralığında olmalı")
	}
	if c.Memory.RecallMinScore < 0 || c.Memory.RecallMinScore > 1 {
		return fmt.Errorf("config: MEMORY_RECALL_MIN_SCORE 0..1 aralığında olmalı (verilen %v)", c.Memory.RecallMinScore)
	}
	if c.Memory.MaxPerUser < 0 {
		return fmt.Errorf("config: MEMORY_MAX_PER_USER negatif olamaz")
	}
	switch c.Knowledge.VectorSearch {
	case "", "auto", "on", "off": // boş = auto (elle kurulan Config değişmezleri için)
	default:
		return fmt.Errorf("config: KNOWLEDGE_VECTOR_SEARCH auto, on veya off olmalı (verilen %q)", c.Knowledge.VectorSearch)
	}
	return nil
}

func getString(key, def string) string {
	if v, ok := os.LookupEnv(key); ok && strings.TrimSpace(v) != "" {
		return strings.TrimSpace(v)
	}
	return def
}

func getInt(key string, def int) int {
	if v, ok := os.LookupEnv(key); ok {
		if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
			return n
		}
	}
	return def
}

// getFloat ondalık ayarları okur (temperature, top_p). Geçersizse varsayılana düşer.
func getFloat(key string, def float64) float64 {
	if v, ok := os.LookupEnv(key); ok {
		if f, err := strconv.ParseFloat(strings.TrimSpace(v), 64); err == nil {
			return f
		}
	}
	return def
}

func getDuration(key string, def time.Duration) time.Duration {
	if v, ok := os.LookupEnv(key); ok {
		if d, err := time.ParseDuration(strings.TrimSpace(v)); err == nil {
			return d
		}
	}
	return def
}

// getBool "true/1/yes/on" değerlerini doğru, diğerlerini yanlış sayar.
func getBool(key string, def bool) bool {
	v, ok := os.LookupEnv(key)
	if !ok {
		return def
	}
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "1", "true", "yes", "on":
		return true
	case "0", "false", "no", "off", "":
		return false
	}
	return def
}

// Memory kalıcı bellek (Faz 10) ayarlarıdır: asistanın kullanıcı hakkında hatırladıkları.
type Memory struct {
	// Enabled false ise bellek hiç kurulmaz: anı getirme, öğrenme ve /api/memories kapalıdır.
	Enabled bool
	// AutoLearn false ise yalnızca kullanıcının elle eklediği anılar tutulur; sohbetten
	// otomatik çıkarım yapılmaz. Otomatik çıkarım her uygun mesajda ek bir LLM çağrısıdır
	// (yerel GPU'da ses hattıyla VRAM için yarışır).
	AutoLearn bool
	// MaxPerUser kullanıcı başına anı üst sınırıdır. 0 = memory.DefaultMaxPerUser.
	MaxPerUser int
	// RecallMinScore bir anının isteme girmesi için gereken en düşük hibrit skordur (0..1).
	// İLK TAHMİN: bilgi tabanıyla aynı ölçek, canlı veriyle ayarlanmadı.
	RecallMinScore float64
	// LearnModel çıkarım için kullanılacak model. Boşsa sohbet modeli kullanılır; büyük bir
	// sohbet modeli (ör. 30b) çıkarımı çok yavaşlatır ve sonraki mesajı geciktirir — küçük bir model verin.
	LearnModel string
}

func splitList(s string) []string {
	var out []string
	for _, p := range strings.Split(s, ",") {
		if p = strings.TrimSpace(p); p != "" {
			out = append(out, p)
		}
	}
	return out
}
