// Command server FIKO-AI Phase 1 HTTP/WebSocket sunucusunu çalıştırır.
//
// Bağımlılık grafiği (elle kurulur; DI kütüphanesi kullanılmaz):
//
//	config → conversation.MemoryRepo → conversation.Service
//	       → ollama.Client (llm.Provider)
//	       → event.MemoryBus
//	       → agent.Agent
//	       → ws.Hub
//	       → api.Server (REST + /ws)
//
// Kapatma sırası önemlidir: agent.Close (üretimleri iptal et) → hub.Close (olayları boşaltıp
// bağlantıları kapat) → api.Shutdown (yeni istek alma, açık HTTP bağlantılarını bitir).
package main

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"fiko-ai/config"
	"fiko-ai/internal/agent"
	"fiko-ai/internal/api"
	"fiko-ai/internal/api/ws"
	"fiko-ai/internal/audit"
	"fiko-ai/internal/claim"
	"fiko-ai/internal/conversation"
	"fiko-ai/internal/event"
	"fiko-ai/internal/knowledge"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/llm/ollama"
	"fiko-ai/internal/memory"
	"fiko-ai/internal/notify"
	"fiko-ai/internal/pdks"
	"fiko-ai/internal/persona"
	"fiko-ai/internal/prompt"
	"fiko-ai/internal/protocol"
	"fiko-ai/internal/settings"
	"fiko-ai/internal/skill"
	"fiko-ai/internal/storage"
	"fiko-ai/internal/storage/mongostore"
	"fiko-ai/internal/stt"
	"fiko-ai/internal/tool"
	"fiko-ai/internal/tts"
	"fiko-ai/internal/user"
	"fiko-ai/internal/workflow"
)

// version uygulama sürümüdür; /api/health yanıtında ve log başlığında görünür.
const version = "0.1.0"

func main() {
	if err := run(); err != nil {
		fmt.Fprintf(os.Stderr, "fiko-ai: %v\n", err)
		os.Exit(1)
	}
}

func run() error {
	cfg, err := config.Load()
	if err != nil {
		return err
	}
	log := newLogger(cfg.Log)
	slog.SetDefault(log)

	log.Info("fiko-ai başlıyor",
		"version", version, "env", cfg.Env, "addr", cfg.HTTP.Addr, "ollama", cfg.Ollama.BaseURL)

	// Kapatma sinyalleriyle iptal edilebilen kök bağlam: WS oturumlarını ve istekleri bu bağlam yönetir.
	rootCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	// Depo seçimi: MongoDB varsa kalıcı, yoksa bellek içi.
	// Düşüş bilinçlidir: Mongo erişilemezse sunucu yine ayakta olur (geçici sohbetler bellekte),
	// ama durumu log'da açıkça belirtiriz ki veri kaybı sürpriz olmasın.
	repo, closeRepo := openStores(rootCtx, cfg, log)
	defer closeRepo()

	convSvc := conversation.NewService(repo.conversations)

	// Performans ayarları: keep_alive modeli VRAM'de tutar (her mesajda yeniden
	// yükleme beklemesini siler), num_ctx tavanı KV cache'in devasa boyutunu engeller.
	// MODEL SEÇİMİNE DOKUNMAZ — kullanıcı arayüzden istediği modeli seçmeye devam eder.
	provider := ollama.New(cfg.Ollama.BaseURL, cfg.Ollama.Model, cfg.Ollama.RequestTimeout).
		WithEmbedModel(cfg.Ollama.EmbedModel).
		WithTunables(ollama.Tunables{
			KeepAlive:  cfg.Ollama.KeepAlive,
			MaxContext: cfg.Ollama.MaxContext,
			NumGPU:     cfg.Ollama.NumGPU,
			NumBatch:   cfg.Ollama.NumBatch,
			NumThread:  cfg.Ollama.NumThread,
		}).
		// Cloud modellerde (`:cloud`) Ollama isteği ollama.com'a proxy'ler ve
		// upstream bağlantısı arada 502 / PROTOCOL_ERROR ile kopabiliyor. Bu geçici
		// hatayı sohbeti öldürmemesi için SINIRLI tekrar deneme açılır. Retry yalnızca
		// HTTP başlıkları gelmeden önce çalışır; akış başladıktan sonra asla tekrarlanmaz.
		WithRetry(cfg.Ollama.MaxAttempts, cfg.Ollama.RetryBase)
	bus := event.NewMemoryBus(log, 256)
	defer bus.Close()

	// Skill Registry: kayıt/doğrulama iş kuralları; olaylar (SkillCreated/Updated/Deleted) bus'a gider.
	skillSvc := skill.NewService(repo.skills, bus)
	defer setupSkillEmbedding(rootCtx, skillSvc, provider, cfg, log)()

	// Bilgi Tabanı (RAG, Faz 8): belge → parça → embedding → hibrit arama. Skill vektörlerinden AYRI servis.
	// Embedding modeli yoksa belgeler yalnızca anahtar kelimeyle aranır (bilinçli düşüş).
	kbSvc := knowledge.NewService(repo.knowledge, bus, knowledge.Options{Logger: log})
	if cfg.Ollama.EmbedModel != "" {
		kbSvc.SetEmbedder(provider, cfg.Ollama.EmbedModel)
	}
	// MongoDB $vectorSearch: index arka planda kurulur; hazır olana (ya da mongot yoksa hep) Go tarafı skorlama.
	defer setupKnowledgeVectorSearch(rootCtx, repo.knowledge, provider, cfg, log)()
	if n, err := kbSvc.Resume(rootCtx); err != nil {
		log.Warn("yarım kalan belgelerin embedding işi sürdürülemedi", "err", err)
	} else if n > 0 {
		log.Info("yarım kalan belgelerin embedding işi sürdürülüyor", "belge", n)
	}
	defer func() {
		cctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := kbSvc.Close(cctx); err != nil {
			log.Warn("bilgi tabanı arka plan işleri zamanında bitmedi", "err", err)
		}
	}()

	// Denetim kaydı: spec "her işlem audit log'a yazılmalı" diyor.
	// Dosya açılamazsa (izin/ro) uygulama ÇÖKMEZ: stdout'a düşer ve uyarı verilir.
	auditLog, err := openAudit(cfg.Audit.Path, log)
	if err != nil {
		return err
	}
	if auditLog != nil {
		defer auditLog.Close()
	}

	// Tool Registry + Executor + izin deposu (Faz 4). Skill, tool sistemine BAĞLANIR:
	// executorConfig.tool varlığı denetlenir ve ExecuteSkill gerçekten çalışır.
	// Denetim logger'ı önce açılır: HER tool çalıştırması kayda geçmelidir (başarılı, hatalı, reddedilmiş).
	tools, err := setupTools(rootCtx, repo.tools, bus, cfg, auditLog, log)
	if err != nil {
		return err
	}
	// ESKİ TOOL SİSTEMİ BAYRAĞI (Faz 1): kapalıyken skill'ler tool'a bağlanmaz;
	// POST /api/skills/:id/execute çalıştırıcı olmadığı için hata döner.
	if cfg.Legacy.Tools {
		bindSkillTools(skillSvc, tools)
	} else {
		log.Info("eski tool sistemi DEAKTİF (FIKO_LEGACY_TOOLS=false): skill→tool bağı kurulmadı")
	}

	// Ayarlar: başlangıçta .env'den gelir, sonra /api/settings ile değiştirilebilir.
	set := settings.New(settings.Values{}, log, bus)
	if _, err := set.Set(settings.Values{
		Model:       cfg.Ollama.Model,
		Temperature: cfg.LLM.Temperature,
		TopP:        cfg.LLM.TopP,
		MaxTokens:   cfg.LLM.MaxTokens,
		ContextSize: cfg.LLM.ContextSize,
	}); err != nil {
		log.Warn("varsayılan ayarlar uygulanamadı", "err", err)
	}
	// KALICILIK: arayüzden seçilen model/ayarlar diske yazılır ve açılışta .env değerlerinin
	// ÜSTÜNE yüklenir. Aksi halde her yeniden başlatmada OLLAMA_MODEL'e dönülür.
	if err := set.EnablePersistence(cfg.SettingsPath); err != nil {
		log.Warn("kayıtlı ayarlar okunamadı; .env varsayılanları kullanılıyor", "path", cfg.SettingsPath, "err", err)
	}

	ag := agent.New(convSvc, provider, prompt.Default(), bus, log, agent.Config{
		MaxHistory:        cfg.Chat.MaxHistoryMessages,
		MaxInputRunes:     cfg.Chat.MaxInputRunes,
		GenerationTimeout: cfg.Chat.GenerationTimeout,
		Timezone:          cfg.Chat.Timezone,
		ContextWindow:     cfg.Chat.DefaultContextTokens,
		MaxIterations:     cfg.Chat.MaxAgentIterations,
		// Ayarlar çalışma zamanında okunur: her üretim güncel model/temperature'ı kullanır.
		OptionsFunc: set.LLMOptions,
		// Sistem istemi de çalışma zamanında okunur: yönetici arayüzden bir istem
		// parçasını değiştirdiğinde bir sonraki mesajdan itibaren geçerli olur.
		PromptEditsFunc: func() (prompt.Overrides, prompt.Disabled) {
			return set.PromptOverrides(), set.PromptDisabled()
		},
		// Model isteğin payload'ında GELMEZ ("boşsa sunucu varsayılanı"). Ayarlardaki
		// modeli buradan bağlıyoruz; bağlanmazsa her üretim OLLAMA_MODEL'e düşer ve
		// ayar ekranından seçilen model kullanılmaz (canlı testte görüldü).
		ModelFunc: set.Model,
		// Yazma/silme onay politikası da çalışma zamanında okunur: kullanıcı ayar
		// ekranından değiştirdiğinde sunucu yeniden başlatmaya gerek kalmadan
		// yeni politika geçerli olur.
		ConfirmFunc: set.ConfirmMode,
	})

	// Tool çağırma (Faz 5): runner + catalog. Olay çıktısı her turun kendi emit'idir.
	//
	// ESKİ TOOL SİSTEMİ BAYRAĞI (Iddia Hafızası, Faz 1): FIKO_LEGACY_TOOLS=false iken ajan
	// hiç tool görmez/çağırmaz. Kod yerinde kalır; bayrak true olunca eskisi gibi çalışır.
	// Skill retrieval yalnızca "hangi tool'lar sunulsun" kararını verdiği için birlikte kapanır.
	if cfg.Legacy.Tools {
		ag.SetTools(
			agentRunner{tools.executor},
			agentCatalog{tools.registry, tools.grants},
		)
		// SKILL RETRIEVAL (Faz 5c): önce skill seçilir, sonra yalnızca o skill'lerin tool'u
		// sunulur. Retrieval boş/hatalıysa tüm izinli tool'lar sunulur (davranış değişmez).
		ag.SetSkills(skillRetriever{skillSvc})
	}
	// BİLGİ TABANI (Faz 8): ilgili belge parçaları sistem istemine girer; tool'dan bağımsızdır.
	ag.SetKnowledge(knowledgeRetriever{svc: kbSvc, minScore: knowledgeMinScore})

	// KALICI BELLEK (Faz 10): kullanıcı hakkında hatırlananlar sistem istemine girer; uygun mesajlardan
	// arka planda otomatik öğrenilir. MEMORY_ENABLED=false ise hiçbir şey bağlanmaz (memSvc nil kalır).
	var memSvc *memory.Service
	var memParts *memoryParts
	if mem := setupMemory(repo.memories, bus, provider, provider, cfg, log); mem != nil {
		memSvc = mem.svc
		memParts = mem
		defer closeMemory(mem, log)
		ag.SetMemory(mem.recaller, mem.learner)
	}
	if llm.SupportsToolCalling(provider) {
		log.Info("ajan tool çağırma ETKİN", "maxIterations", cfg.Chat.MaxAgentIterations, "skillRetrieval", true)
	} else {
		log.Warn("sağlayıcı tool çağırmayı desteklemiyor; ajan tool'suz çalışır")
	}

	// Konuşma tanıma (Phase 6): STT_URL boşsa sesli giriş kapalıdır.
	// Motor seçimi kullanıcıya aittir (STT_PROVIDER); ikisi de Recognizer arayüzünü uygular.
	var sttSvc *stt.Service
	if cfg.STT.Enabled && cfg.STT.URL != "" {
		sttOpt := stt.Options{
			URL: cfg.STT.URL, Language: cfg.STT.Language, Timeout: cfg.STT.Timeout,
			APIKey: cfg.STT.APIKey, Model: cfg.STT.Model,
		}
		provider := strings.ToLower(cfg.STT.Provider)
		var rec stt.Recognizer
		if provider == "openai" {
			rec = stt.NewWhisperAPI(sttOpt)
		} else {
			provider = "whispercpp"
			rec = stt.NewWhisper(sttOpt)
		}
		if rec == nil {
			log.Warn("STT_URL ayarlı ama motor kurulamadı; sesli giriş kapalı")
		} else {
			sttSvc = stt.New(rec, stt.Limits{MaxSeconds: cfg.STT.MaxSeconds, MaxBytes: cfg.STT.MaxBytes})
			log.Info("sesli giriş ETKİN", "motor", provider, "dil", cfg.STT.Language, "maksSn", cfg.STT.MaxSeconds)
		}
	} else {
		log.Info("sesli giriş KAPALI (STT_URL boş): voice.start istekleri 'yapılandırılmamış' ile reddedilir")
	}

	// Ses üretimi (Phase 7): TTS_URL boşsa sesli çıkış kapalıdır (metin akışı normal çalışır).
	// İki protokol desteklenir; motor seçimi TTS_PROVIDER ile yapılır.
	var synth tts.Synthesizer
	if cfg.TTS.Enabled && cfg.TTS.URL != "" {
		opts := tts.Options{
			URL: cfg.TTS.URL, APIKey: cfg.TTS.APIKey, Model: cfg.TTS.Model,
			Voice: cfg.TTS.Voice, Format: cfg.TTS.Format, Speed: cfg.TTS.Speed, Timeout: cfg.TTS.Timeout,
		}
		switch strings.ToLower(cfg.TTS.Provider) {
		case "piper":
			synth = tts.NewPiper(opts)
		default:
			synth = tts.NewOpenAI(opts)
		}
		if synth != nil {
			log.Info("sesli çıkış ETKİN", "motor", cfg.TTS.Provider, "ses", cfg.TTS.Voice, "bicim", cfg.TTS.Format)
		} else {
			log.Warn("TTS_URL ayarlı ama motor kurulamadı; sesli çıkış kapalı")
		}
	} else {
		log.Info("sesli çıkış KAPALI (TTS_URL boş): tts.mode istekleri 'yapılandırılmamış' ile reddedilir")
	}
	hub := ws.NewHubWithVoice(ag, sttSvc, synth, log, ws.Options{
		PingInterval:   cfg.WS.PingInterval,
		PongWait:       cfg.WS.PongWait,
		WriteWait:      cfg.WS.WriteWait,
		OutboundBuffer: cfg.WS.OutboundBuffer,
	})

	// BELLEK ONAY AKIŞI: LLM karar veremediğinde soru kullanıcıya sorulur, cevabı o verir.
	// Bu blok, üç parçayı birbirine bağlar:
	//   ajan (soruyu üretir) → depo (TTL'li bekletir) → hub (istemciye gönderir)
	//
	// KOŞUL DÜZELTMESİ: buradaki `mem.learner != nil` kontrolü SORU AKIŞINI da
	// kapatıyordu. Oysa `memory.ask` tool'u arka plan öğrenmeyle ilgisizdir;
	// MEMORY_AUTO_LEARN=false iken de kullanıcıya sorulabilmelidir. Bu yüzden
	// yalnızca soru deposu (questions) aranır; learner nil olabilir.
	if mem := memParts; mem != nil && mem.questions != nil {
		// Yayıncı: sorular KULLANICININ tüm bağlantılarına gider (sohbete bağlı değildir).
		mem.emitter = func(userID, conversationID string, payload protocol.MemoryQuestionPayload) {
			hub.BroadcastToUser(userID, conversationID, protocol.ServerMemoryQuestion, payload)
		}
		// memory.ask TOOL'u: soruyu MODEL, sohbet sırasında bu tool çağrısıyla üretir.
		// Ajanın kendi soru yolu (MemoryQuestionSink) YOKTUR; iki yol birbirini bölerdi.
		if err := mem.RegisterTools(rootCtx, tools.registry, log); err != nil {
			log.Warn("bellek tool'ları kaydedilemedi", "err", err)
		}
		hub.SetMemory(mem.questions, memAnswerFunc(mem))
		log.Info("bellek tool'ları ETKİN: memory.remember / memory.update / memory.recall / memory.ask")
	}

	// Hesap servisi: depo `st` içinden gelir (MongoDB veya bellek içi).
	userSvc := newUserService(rootCtx, repo.users, cfg, log)

	// IDDIA HAFIZASI (Faz 3): sohbetten ekip ortak kayıt yazma. FIKO_CLAIMS_WRITE=false iken
	// hiçbir şey bağlanmaz; açıkken de yazma yalnızca gerçek oturum kimliğiyle çalışır.
	// `userSvc`'ye bağlıdır (kimlik doğrulama), bu yüzden newUserService'ten SONRA kurulur.
	// Özet, yazma arka planda bittiğinde kullanıcının TÜM açık bağlantılarına gider (`claim.summary`);
	// sohbete bağlı değildir ama hangi sohbetten geldiği conversationId olarak taşınır.
	claimEmit := func(userID, conversationID string, p protocol.ClaimSummaryPayload) {
		hub.BroadcastToUser(userID, conversationID, protocol.ServerClaimSummary, p)
	}
	var claimWriteSvc *claim.Service
	if cl := setupClaims(repo.claims, repo.claimAudit, provider, provider, userSvc, cfg, log, claimEmit); cl != nil {
		defer closeClaims(cl)
		ag.SetClaims(cl.capturer)
		claimWriteSvc = cl.svc
	}
	// Yönetim (Faz 5): /api/claims. Yazma bayrağından bağımsız; yetki yalnızca doğrulanmış hesaptan.
	claimMgr, claimIdent := setupClaimManager(claimWriteSvc, repo.claims, repo.claimAudit, provider, provider, userSvc, cfg, log)
	// Okuma (Faz 4): FIKO_CLAIMS_READ=false iken nil döner. Yazmadan bağımsız bayraktır.
	if rd := setupClaimReader(repo.claims, repo.claimAudit, provider, provider, userSvc, cfg, log); rd != nil {
		ag.SetClaimReader(rd)
	}
	// PDKS (Personel Devam Kontrol): bölüm, personel, vardiya, giriş/çıkış/mola
	// olayları, izin ve devam raporları.
	//
	// Neden ayrı servis? Günlük çalışma süresi giriş/çıkış OLAYLARINDAN
	// hesaplanır (tek "günlük kayıt" alanı tutmak gerçekçi değildir: öğle arası
	// çıkış-giriş, unutulmuş mola, eksik çıkış kaydı olur). Bu hesap `biz`
	// paketinde değil, burada yaşar.
	pdksSvc := pdks.New(pdks.Options{Repo: repo.attendance, Logger: log})
	log.Info("PDKS ETKİN: bölüm, personel, vardiya, giriş/çıkış, mola, izin, raporlar")

	// KULLANICILAR ARASI BİLDİRİM: AI başka kullanıcılara bildirim gönderebilir ve
	// soru sorabilir. Bu blok üç parçayı birbirine bağlar:
	//   model (notify.* tool'ları) → servis (sınırlar + katılım) → hub (WS + tekrar)
	//
	// SIRA ÖNEMLİ: `userSvc`'ye bağlıdır (gönderen adı + alıcı araması), bu yüzden
	// newUserService'ten SONRA kurulur. Hub zaten hazırdır (bellek bloğunda).
	notifySvc := notify.NewService(notify.Options{
		Repo:       repo.notifications,
		Users:      notifyUsers{svc: userSvc},
		Recipients: notifyRecipients{svc: userSvc},
		Limits:     notify.DefaultLimits(),
		Logger:     log,
	})
	// Sink: anlık bildirim WS'e gider. Bağlanmadan önce gönderilenler de kaydedilir;
	// bağlantı kurulunca replayNotifications geri yükler (veri kaybı olmaz).
	notifySvc.SetSink(hub)
	hub.SetNotify(notifyBridgeFor(notifySvc, userSvc))
	registerNotifyTools(rootCtx, notifySvc, tools.registry, log)

	// PDKS araçları: personel/giriş-çıkış/mola/rapor. SİLME araçları RiskHigh
	// olduğundan kullanıcının onay politikasına göre dialoga düşer
	// (bkz. pdks/tools.go başlığı ve cmd/server/pdks.go).
	registerPdksTools(rootCtx, pdksSvc, tools.registry, log)
	log.Info("bildirim özelliği ETKİN",
		"depo", notifyRepoName(repo.notifications),
		"saatteEnFazla", notifySvc.Limits().MaxPerHour,
		"aliciBekleyenSoru", notifySvc.Limits().MaxPendingPerReceiver)

	// KULLANICI PROFİLLERİ (yönetici tanımlı davranış).
	//
	// Ne yapar? Yönetici "Proje Yöneticisi" gibi bir profil yazar ve kullanıcılara
	// atar; ajan her turda o kullanıcının profil kurallarını sistem istemine ekler.
	// Böylece "bu kişi proje yöneticisidir, proje eklerken eksiksiz bilgi alsın" gibi
	// davranış tanımları KOD DEĞİŞTİRMEDEN uygulanabilir.
	//
	// Bağımlılık yönü: persona → agent/prompt BİLMEZ; burada persona'yı ajanın
	// PersonaSource arayüzüne çevirerek bağlıyoruz (notify bridge deseni).
	personaSvc, perr := persona.NewService(repo.personas, log)
	if perr != nil {
		log.Warn("kullanıcı profilleri kurulamadı (profil kuralları kapalı)", "err", perr)
	} else {
		ag.SetPersonas(personaSourceFor(personaSvc))
		log.Info("kullanıcı profilleri ETKİN", "depo", personaRepoName(repo.personas))
	}

	// Agent Builder / Akışlar (Faz 9): kod yazmadan çizilen iş akışları + yürütme kaydı.
	// Yetenekler Mevcut servislerden bağlanır (bkz. workflows.go); Recover açılışta
	// "running" kalmış yürütmeleri kapatır (sunucu çökmüşse kayıtta "çalışıyor" kalmaz).
	wfDeps := workflowDeps(provider, tools, kbSvc, convSvc, skillSvc, set.Model, set.LLMOptions)
	logUnavailableWorkflowNodes(wfDeps, log)
	wfSvc := workflow.NewService(repo.workflows, wfDeps, workflow.Options{
		Logger:        log,
		MaxConcurrent: cfg.Workflow.MaxConcurrent,
		RunTimeout:    cfg.Workflow.RunTimeout,
		OnFinish: func(e workflow.Execution) {
			// Denetim kaydı: kim hangi akışı çalıştırdı, sonucu ne oldu?
			log.Info("akış yürütmesi bitti",
				"execution", e.ID, "workflow", e.WorkflowID, "status", e.Status,
				"latencyMs", e.LatencyMs, "err", e.Error)
		},
	})
	// Kapanış: süren yürütmeler iptal edilir ve bitmeleri beklenir (yeni yürütme reddedilir).
	defer func() {
		cctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := wfSvc.Close(cctx); err != nil {
			log.Warn("akış servisi zamanında kapanmadı", "err", err)
		}
	}()
	if n, err := wfSvc.Recover(rootCtx); err != nil {
		log.Warn("yarım kalan akış yürütmeleri kapatılamadı", "err", err)
	} else if n > 0 {
		log.Warn("sunucu yeniden başladı: yarım kalan yürütmeler başarısız olarak işaretlendi", "adet", n)
	}

	// Hesap servisi: depo `st` içinden gelir (MongoDB veya bellek içi).
	// (userSvc yukarıda, bildirim bloğundan önce kuruldu.)

	// ESKİ TOOL SİSTEMİ BAYRAĞI (Faz 1): FIKO_LEGACY_TOOLS=false iken /api/tools ve
	// /api/skills/:id/execute uçları nil bağımlılık yüzünden 503 döner (toolsAvailable).
	// Route kodu yerinde kalır; bayrak true olunca bağımlılıklar verilir ve eskisi gibi çalışır.
	// Grants BİLEREK her durumda verilir: üye/yönetici yönetimi (/admin, /me/page) buna bağlıdır.
	var apiTools *tool.Registry
	var apiToolExec *tool.Executor
	if cfg.Legacy.Tools {
		apiTools, apiToolExec = tools.registry, tools.executor
	}

	srv := api.New(rootCtx, api.Config{
		Env:             cfg.Env,
		Version:         version,
		AllowedOrigins:  cfg.HTTP.AllowedOrigins,
		DefaultUserID:   cfg.Chat.DefaultUserID,
		AuthKey:         cfg.Auth.APIKey,
		UsersEnabled:    cfg.Users.Enabled,
		DefaultModel:    cfg.Ollama.Model,
		BodyLimit:       int(cfg.WS.MaxMessageBytes),
		MaxPageSize:     200,
		DefaultPageSize: 50,
		// HTTPS yayını: sertifika verilmişse sunucu TLS konuşur, statik dizin
		// verilmişse arayüz de aynı origin'den sunulur (ayrı nginx gerekmez).
		TLSCertFile: cfg.HTTP.TLSCertFile,
		TLSKeyFile:  cfg.HTTP.TLSKeyFile,
		StaticDir:   cfg.HTTP.StaticDir,
		// Sistem ekranı: hangi motorlar açık, hangi ayarlar eksik.
		System: systemFeatures(cfg),
	}, api.Deps{
		Conversations: convSvc,
		LLM:           provider,
		Hub:           hub,
		Bus:           bus,
		Settings:      set,
		Skills:        skillSvc,
		Tools:         apiTools,
		ToolExec:      apiToolExec,
		Grants:        tools.grants,
		Knowledge:     kbSvc,
		Workflows:     wfSvc,
		Memory:        memSvc,
		Notify:        notifySvc,
		Personas:      personaSvc,
		Users:         userSvc,
		Pdks:          pdksSvc,
		Audit:         auditLog,
		Claims:        claimMgr,
		ClaimIdentity: claimIdent,
	}, log)

	// Ollama erişilebilir mi? Ulaşılamıyorsa sunucu yine de ayakta olur (sohbet dışı uçlar çalışır).
	warnIfOllamaDown(rootCtx, provider, cfg, log)

	serveErr := make(chan error, 1)
	go func() {
		// ShutdownWithContext sonrası Fiber http.ErrServerClosed döner; bu normal kapanıştır.
		// Listen, TLS yapılandırması varsa HTTPS konuşur (bkz. api.Config.TLSCertFile).
		if err := srv.Listen(cfg.HTTP.Addr); err != nil && !errors.Is(err, http.ErrServerClosed) {
			serveErr <- err
			return
		}
		serveErr <- nil
	}()

	select {
	case err := <-serveErr:
		if err != nil {
			return fmt.Errorf("http dinleme hatası: %w", err)
		}
		return nil
	case <-rootCtx.Done():
		log.Info("kapatma sinyali alındı, kapatılıyor")
	}

	stop() // ikinci sinyale bağlamımızı zorla iptal etmesin
	shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.HTTP.ShutdownTimeout)
	defer cancel()
	shutdownWith(shutdownCtx, log, ag, hub, httpCloser{srv})
	log.Info("fiko-ai durdu")
	return nil
}

// closer kapanışa katılan bileşenlerin ortak arayüzüdür (testlerde sahte uygulanır).
type closer interface{ Close(context.Context) error }

// openAudit denetim logger'ını açar. Dosya açılamazsa süreç ÇÖKMEZ:
// kayıt stdout'a düşer ve kullanıcı uyarılır (denetim kaydı üretim akışını durdurmamalı).
func openAudit(path string, log *slog.Logger) (*audit.Logger, error) {
	l, err := audit.New(audit.Options{Path: path, Logger: log})
	if err != nil {
		log.Warn("denetim dosyası açılamadı — kayıtlar stdout'a yazılacak", "path", path, "err", err)
		fallback, ferr := audit.New(audit.Options{Logger: log})
		if ferr != nil {
			return nil, err // stdout bile açılamadıysa gerçek hatadır
		}
		return fallback, nil
	}
	if path != "" {
		log.Info("denetim kaydı etkin", "path", path)
	}
	return l, nil
}

// stores kalıcı depoların toplamıdır; hepsi AYNI kararla (Mongo ya da bellek içi) seçilir,
// böylece konuşmalar kalıcıyken skill'lerin bellekte kalması gibi tutarsız bir durum oluşmaz.
type stores struct {
	conversations conversation.Repository
	skills        skill.Repository
	tools         tool.Repository
	knowledge     knowledge.Repository
	workflows     workflow.Store
	memories      memory.Repository
	users         user.Store
	// notifications kullanıcılar arası bildirim deposudur.
	//
	// Neden ayrı alan? Diğer depolar tekil kaynak tutar (konuşma, anı…); bildirim
	// KULLANICIYA aittir ve alıcı/gönderen sorguları taşır. Mongo yoksa bellek içi
	// depoya düşer (notifications MISSING olursa yedek yol kurulur).
	notifications notify.Repository
	// personas, kullanıcı davranış profilleri deposudur (yönetici tanımı).
	//
	// Neden aynı sefer seçilir? Bellekte çalışan kurulumda profiller de bellekte
	// olmalı; Mongo yoksa "Mongo profil + bellek anı" gibi karışık bir durum
	// oluşmasın.
	personas persona.Repository
	// attendance, PDKS (Personel Devam Kontrol) deposudur: bölüm, personel, vardiya,
	// giriş/çıkış/mola olayları ve izinler.
	//
	// Neden ayrı alan? `biz` deposu genel kayıt yönetimidir; PDKS'nin günlük süre
	// hesabı (olay sırasından türetilir) kendi deposunu ve indekslerini ister.
	// Mongo yoksa da bellek içi devreye girer (sunucu ayakta kalır).
	attendance pdks.Repository
	// claims / claimAudit Iddia Hafızası depolarıdır (ekip ortak kayıtlar + denetim geçmişi).
	claims     claim.Repository
	claimAudit claim.AuditRepository
}

func memoryStores() stores {
	return stores{
		conversations: conversation.NewMemoryRepo(),
		skills:        skill.NewMemoryRepo(),
		tools:         tool.NewMemoryRepo(),
		knowledge:     knowledge.NewMemoryRepo(),
		workflows:     workflow.NewMemoryRepo(),
		memories:      memory.NewMemoryRepo(),
		notifications: notify.NewMemoryRepository(),
		personas:      persona.NewMemoryRepository(),
		users:         user.NewMemoryStore(),
		attendance:    pdks.NewMemoryRepo(),
		claims:        claim.NewMemoryRepo(),
		claimAudit:    claim.NewMemoryAuditRepo(),
	}
}

// openStores MongoDB'ye bağlanır; başarısız olursa bellek içi depolara düşer.
// MONGO_ENABLED=false ile düşüş zorlanabilir (test/tanılama).
func openStores(ctx context.Context, cfg config.Config, log *slog.Logger) (stores, func()) {
	noop := func() {}

	if enabled := os.Getenv("MONGO_ENABLED"); strings.EqualFold(enabled, "false") {
		log.Info("MongoDB devre dışı (MONGO_ENABLED=false), bellek içi depolar kullanılıyor")
		return memoryStores(), noop
	}

	conn, err := storage.Connect(ctx, storage.Config{
		URI:             cfg.Mongo.URI,
		Database:        cfg.Mongo.Database,
		ConnectTimeout:  cfg.Mongo.ConnectTimeout,
		ServerSelection: cfg.Mongo.ConnectTimeout,
	})

	if err != nil {
		log.Warn("MongoDB'ye bağlanılamadı — BELLEK İÇİ depolara düşüldü, konuşmalar, skill'ler VE bilgi tabanı belgeleri sunucu kapanınca kaybolacak",
			"uri", redactURI(cfg.Mongo.URI), "err", err)
		return memoryStores(), noop
	}

	log.Info("MongoDB bağlandı", "uri", redactURI(cfg.Mongo.URI), "database", cfg.Mongo.Database)
	closeFn := func() {
		closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := conn.Close(closeCtx); err != nil {
			log.Warn("MongoDB kapatılamadı", "err", err)
		}
	}
	// Hesap deposu: bu satır eksikti → users=nil → hesap servisi kurulamıyor, giriş ekranı hiç açılmıyordu.
	userRepo := mongostore.NewUserRepo(conn.Database())
	if err := userRepo.EnsureIndexes(ctx); err != nil {
		log.Warn("kullanıcı indeksleri kurulamadı", "err", err)
	}
	personaRepo := mongostore.NewPersonaRepo(conn.Database())
	if err := personaRepo.EnsureIndexes(ctx); err != nil {
		log.Warn("profil indeksleri kurulamadı", "err", err)
	}
	// PDKS indeksi kritiktir: (empId, dayOf, at) bileşik indeksi olmadan günlük
	// rapor her çağrıda koleksiyonu tam tarar ve panel yavaşlar.
	pdksRepo := mongostore.NewPdksRepo(conn.Database())
	if err := pdksRepo.EnsureIndexes(ctx); err != nil {
		log.Warn("PDKS indeksleri kurulamadı", "err", err)
	}
	return stores{
		conversations: mongostore.New(conn.Database()),
		skills:        mongostore.NewSkillRepo(conn.Database()),
		tools:         mongostore.NewToolRepo(conn.Database()),
		knowledge:     mongostore.NewKnowledgeRepo(conn.Database()),
		workflows:     mongostore.NewWorkflowRepo(conn.Database()),
		memories:      mongostore.NewMemoryRepo(conn.Database()),
		notifications: mongostore.NewNotifyRepo(conn.Database()),
		personas:      personaRepo,
		users:         userRepo,
		attendance:    pdksRepo,
		claims:        mongostore.NewClaimRepo(conn.Database()),
		claimAudit:    mongostore.NewClaimAuditRepo(conn.Database()),
	}, closeFn
}

// redactURI URI'deki kimlik bilgilerini gizler (log'a sır yazmayalım).
func redactURI(uri string) string {
	if i := strings.Index(uri, "@"); i > 0 {
		if j := strings.Index(uri, "://"); j >= 0 && j+3 < i {
			return uri[:j+3] + "***" + uri[i:]
		}
		return "***" + uri[i:]
	}
	return uri
}

// httpCloser api.Server'ı closer sözleşmesine uyarlar.
// NOT: bağlamsız bir Close() metodu bilerek eklenmedi — zaman aşımı olmayan kapanış,
// HTTP_SHUTDOWN_TIMEOUT'u anlamsız kılar.
type httpCloser struct{ srv *api.Server }

func (h httpCloser) Close(ctx context.Context) error { return h.srv.Shutdown(ctx) }

// shutdownWith kapatma sırasını yürütür ve BU SIRANIN ÖNEMLİ olduğunu testte korur:
//
//  1. agent.Close — süren üretimleri iptal eder; kısmi yanıtlar "interrupted" olarak kaydedilir.
//  2. hub.Close    — o "interrupted" olaylarını kuyruğa alıp bağlantıları boşaltarak kapatır.
//  3. http.Shutdown — yeni istekleri reddeder, açık HTTP bağlantılarını süre dolana kadar bekler.
//
// Sıra tersine çevrilirse istemci "kaybolmuş" bir üretim görür ve kısmi cevap kaybolur.
// Bir bileşen hata verse de zincir kırılmaz: hata loglanır, kalan bileşenler kapanır
// (aksi hâlde süreç açık dinleyiciyle çıkar).
func shutdownWith(ctx context.Context, log *slog.Logger, ag, hub, srv closer) {
	steps := []struct {
		name string
		fn   closer
	}{
		{"ajan kapatılamadı", ag},
		{"ws hub kapatılamadı", hub},
		{"http kapatılamadı", srv},
	}
	for _, s := range steps {
		if err := s.fn.Close(ctx); err != nil {
			log.Warn(s.name, "err", err)
		}
	}
}

// newLogger yapılandırmaya göre slog logger'ı kurar (text: insan, json: makine).
func newLogger(c config.Log) *slog.Logger {
	var level slog.Level
	switch c.Level {
	case "debug":
		level = slog.LevelDebug
	case "warn":
		level = slog.LevelWarn
	case "error":
		level = slog.LevelError
	default:
		level = slog.LevelInfo
	}
	opts := &slog.HandlerOptions{Level: level}
	if c.Format == "json" {
		return slog.New(slog.NewJSONHandler(os.Stdout, opts))
	}
	return slog.New(slog.NewTextHandler(os.Stdout, opts))
}

// warnIfOllamaDown başlangıçta LLM'e kısa bir sorgu atar; erişilemiyorsa yalnızca uyarır.
func warnIfOllamaDown(ctx context.Context, p *ollama.Client, cfg config.Config, log *slog.Logger) {
	checkCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	models, err := p.ListModels(checkCtx)
	if err != nil {
		log.Warn("Ollama'ya ulaşılamadı; sohbet özellikleri çalışmayabilir. Ollama çalışıyor mu?",
			"baseUrl", cfg.Ollama.BaseURL, "err", err)
		return
	}
	names := make([]string, 0, len(models))
	for _, m := range models {
		names = append(names, m.Name)
	}
	log.Info("Ollama hazır", "baseUrl", cfg.Ollama.BaseURL, "varsayılanModel", cfg.Ollama.Model, "modeller", names)
	if cfg.Ollama.Model == "" {
		log.Warn("OLLAMA_MODEL ayarlanmamış: sohbet istekleri model belirtmeden başarısız olur")
	}
}

// newUserService hesap servisini verilen depo üzerinden kurar.
//
// Neden depo dışarıdan gelir? MongoDB bağlantısı ve bellek içi depo AYRI yollarda
// kurulur; ikisi de user.Store arayüzünü sağlar, servis bunların farkını bilmez.
func newUserService(ctx context.Context, store user.Store, cfg config.Config, log *slog.Logger) *user.Service {
	svc, err := user.NewService(store, user.ServiceOptions{TTL: sessionTTL(cfg.Users.SessionTTL, log)})
	if err != nil {
		log.Warn("hesap servisi kurulamadı", "err", err)
		return nil
	}

	// İlk kurulum yöneticisi. Parola boşsa hesap OLUŞTURULMAZ — rastgele bir
	// parola üretmek kullanıcıyı kilitlerdi.
	if cfg.Users.Enabled && strings.TrimSpace(cfg.Users.BootstrapPassword) != "" {
		if u, err := svc.EnsureBootstrap(ctx, cfg.Users.BootstrapUser, cfg.Users.BootstrapPassword); err == nil {
			log.Info("yönetici hesabı hazır", "username", u.Username)
		} else {
			log.Warn("yönetici hesabı oluşturulamadı", "err", err)
		}
	}
	return svc
}

// sessionTTL FIKO_SESSION_TTL değerini oturum ömrüne çevirir ("720h", "24h" gibi).
//
// Boş değer varsayılanı (0 → serviste 30 gün) seçer. Geçersiz ya da sıfır/negatif değer
// SESSİZCE yutulmaz: uyarı yazılır ve varsayılana düşülür; böylece yazım hatası
// oturumları anında sona erdirmez.
func sessionTTL(raw string, log *slog.Logger) time.Duration {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0
	}
	d, err := time.ParseDuration(raw)
	if err != nil || d <= 0 {
		log.Warn("FIKO_SESSION_TTL geçersiz; varsayılan oturum ömrü kullanılacak", "değer", raw, "err", err)
		return 0
	}
	return d
}
