// Package api HTTP taşıma katmanıdır: Fiber uygulamasını kurar, REST uçlarını tanımlar
// ve WebSocket uç noktasını bağlar. İş kuralları bu katmanda değildir; uçlar yalnızca
// istek doğrular, ilgili servise devreder ve hataları JSON gövdesine çevirir.
// Bağımlılıklar tek yönlüdür: api → {conversation, llm, ws}.
package api

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"

	"fiko-ai/internal/api/ws"
	"fiko-ai/internal/audit"
	"fiko-ai/internal/conversation"
	"fiko-ai/internal/event"
	"fiko-ai/internal/knowledge"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/memory"
	"fiko-ai/internal/notify"
	"fiko-ai/internal/persona"
	"fiko-ai/internal/settings"
	"fiko-ai/internal/skill"
	"fiko-ai/internal/tool"
	"fiko-ai/internal/user"
	"fiko-ai/internal/workflow"
)

// WSPath WebSocket uç noktasının yoludur (frontend proxy'si bunu kullanır).
const WSPath = "/ws"

// Config HTTP katmanının ayarlarıdır. Sıfır değerler varsayılanla değiştirilir.
type Config struct {
	Env             string
	Version         string
	AllowedOrigins  []string // boşsa tüm origin'lere izin verilir
	DefaultUserID   string   // kimlik doğrulama yokken (Phase 1) kullanılan kullanıcı
	DefaultModel    string   // yapılandırılmış varsayılan LLM modeli (yalnızca bildirilir)
	DefaultPageSize int
	MaxPageSize     int
	BodyLimit       int
	ReadTimeout     time.Duration
	WriteTimeout    time.Duration
	IdleTimeout     time.Duration

	// TLSCertFile / TLSKeyFile boş değilse sunucu HTTPS konuşur (HTTP değil).
	//
	// Neden gerekli? (canlı 05.10.2026) Arayüz ve API AYNI origin'de yayınlanır
	// (fikoyt.com:4200). Tarayıcı https:// sayfasından http:// API'ye çağrıyı
	// "mixed content" olarak ENGELLER. Bu yüzden API de aynı portta TLS konuşmalıdır.
	TLSCertFile string
	TLSKeyFile  string

	// StaticDir, üretilen arayüz dosyalarının (Angular build) sunulacağı dizindir.
	// Boşsa statik dosya servisi yapılmaz (yalnız API).
	//
	// Amaç: ayrı bir web sunucusu (nginx) KURMADAN tek süreçte her şeyi yayınlamak.
	// /api ve /ws zaten burada; arayüz de aynı origin'den gelmelidir.
	StaticDir string

	// AuthKey, isteklerin yetkilendirilmesi için gereken gizli anahtardır.
	//
	// BOŞSA doğrulama yapılmaz (yerel geliştirme akışı bozulmaz) ve sistem AÇIK
	// kabul edilir; bu durum GET /api/system içinde bildirilir. Doluysa eksik veya
	// yanlış X-Fiko-Auth başlığı 401 döner.
	//
	// Not: bu TEK bir paylaşılan anahtardır (API key). Kullanıcı bazlı oturum, rol
	// ve yetki hiyerarşisi KAPSAM DIŞIDIR — bkz. docs/MIMARI.md.
	AuthKey string

	// UsersEnabled true ise üyelik (kayıt/giriş) sistemi AÇIKTIR: istekler
	// oturum token'ı ile yapılır ve X-Fiko-User başlığı YOK SAYILIR.
	// false ise geliştirme akışı korunur (başlık + API anahtarı).
	UsersEnabled bool

	// System, GET /api/system ekranının göstereceği motor/özellik durumlarıdır.
	// Neden config kopyası? API katmanı config paketine BAĞIMLI olmamalıdır (katman
	// kuralı: api yalnızca transport). Değerler açılışta main tarafından doldurulur.
	System []FeatureStatus
}

func (c *Config) defaults() {
	if c.DefaultUserID == "" {
		c.DefaultUserID = "local"
	}
	if c.DefaultPageSize < 1 {
		c.DefaultPageSize = 50
	}
	if c.MaxPageSize < c.DefaultPageSize {
		c.MaxPageSize = 200
	}
	if c.BodyLimit < 1 {
		c.BodyLimit = 1 << 20 // 1 MiB
	}
	if c.ReadTimeout <= 0 {
		c.ReadTimeout = 30 * time.Second
	}
	if c.WriteTimeout <= 0 {
		c.WriteTimeout = 60 * time.Second
	}
	if c.IdleTimeout <= 0 {
		c.IdleTimeout = 120 * time.Second
	}
}

// Deps uçların ihtiyaç duyduğu bileşenlerdir.
// Hub nil ise WS uç noktası bağlanmaz; LLM nil ise /api/models 503 döner;
// Bus nil ise olay yayınlanmaz; Conversations zorunludur.
// Settings, Skills, Tools, ToolExec, Grants, Memory ve Audit nil olabilir: o zaman ilgili
// uç 503 döner / kayıt alınmaz.
type Deps struct {
	Conversations *conversation.Service
	LLM           llm.Provider
	Hub           *ws.Hub
	Bus           event.Bus
	Settings      *settings.Service
	Skills        *skill.Service
	Tools         *tool.Registry
	ToolExec      *tool.Executor
	Grants        *tool.GrantStore
	Knowledge     *knowledge.Service
	Workflows     *workflow.Service
	Memory        *memory.Service
	// Notify kullanıcılar arası bildirim servisidir; nil ise özellik kapalıdır
	// (/api/notifications 503 döner ve notify.* tool'ları kaydedilmez).
	Notify *notify.Service
	// Personas kullanıcı davranış profilleri servisidir; nil ise yönetici
	// profili tanımlayamaz ve profil kuralları isteme hiç girmez.
	Personas *persona.Service
	// Users hesap/oturum servisidir; nil ise üyelik sistemi kapalıdır.
	Users *user.Service
	Audit *audit.Logger
}

// Server Fiber uygulamasını ve onun bağımlılıklarını taşır.
type Server struct {
	app  *fiber.App
	cfg  Config
	deps Deps
	log  *slog.Logger

	ctx     context.Context // sunucu ömrü; WS oturumlarını da kapatır
	started time.Time
}

// New HTTP sunucusunu kurar. ctx sunucu ömrüdür: iptal edildiğinde WS oturumları kapanır.
func New(ctx context.Context, cfg Config, deps Deps, log *slog.Logger) *Server {
	if log == nil {
		log = slog.Default()
	}
	cfg.defaults()
	if deps.Conversations == nil {
		panic("api: Deps.Conversations zorunlu")
	}

	app := fiber.New(fiber.Config{
		AppName:               "fiko-ai " + cfg.Version,
		BodyLimit:             cfg.BodyLimit,
		ReadTimeout:           cfg.ReadTimeout,
		WriteTimeout:          cfg.WriteTimeout,
		IdleTimeout:           cfg.IdleTimeout,
		DisableStartupMessage: true,
		ErrorHandler:          errorHandler(log),
	})

	s := &Server{app: app, cfg: cfg, deps: deps, log: log, ctx: ctx, started: time.Now()}
	s.middleware()
	s.routes()
	if deps.Hub != nil {
		ws.Mount(app, WSPath, ctx, deps.Hub, ws.FiberConfig{
			MaxMessageBytes: int64(cfg.BodyLimit),
			AllowedOrigins:  cfg.AllowedOrigins,
			UserResolver:    s.resolveUserID,
			AuthChecker:     s.checkAuth,
		})
	}
	s.static()
	return s
}

// static, üretilen arayüzü (Angular build) aynı origin'den sunar.
//
// Neden? (canlı 05.10.2026) Ayrı bir web sunucusu (nginx) kurmadan yayın yapmak
// istendiğinde arayüzün de AYNI origin'de olması gerekir: https:// sayfa, http://
// API'yi "mixed content" olarak engeller. Tek origin = hem CORS hem mixed content
// sorunu yok.
//
// NEDEN Fiber'ın app.Static/SendFile'ı DEĞİL? (canlı hata 05.10.2026)
// Fiber'ın statik middleware'i dosyayı AÇIP TUTAR. Windows'ta bu, dosya
// tanıtıcısını kilitler: test geçici dizini silemiyor, düzenli `npm run build`
// sonrası dosya DEĞİŞTİRİLEMEZ ("dosya başka bir işlem tarafından kullanılıyor")
// ve sunucu yeniden başlatılmadan yeni build'i sunmaz. Üretimde sessizce eski
// arayüz yayınlamak demektir. Burada dosya OKUNUR ve HEMEN KAPATILIR.
//
// SPA FALLBACK neden şart? Angular yönlendirme (route) uygulamasıdır: /tools,
// /ayarlar gibi adresler dosya DEĞİL, istemci tarafı rotasıdır. Statik sunucu
// 404 dönerse sayfa açılmaz. Bu yüzden: var olan dosya → dosya, yoksa → index.html.
// DİKKAT: /api ve /ws fallback'e DÜŞMEMELİ (yoksa hata gövdesi HTML olur).
func (s *Server) static() {
	dir := strings.TrimSpace(s.cfg.StaticDir)
	if dir == "" {
		return
	}
	if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
		s.log.Warn("statik arayüz dizini bulunamadı; yalnız API yayınlanıyor", "dizin", dir, "err", err)
		return
	}
	root, err := filepath.Abs(dir)
	if err != nil {
		s.log.Warn("statik dizin çözülemedi", "dizin", dir, "err", err)
		return
	}
	app := s.app
	app.Use(func(c *fiber.Ctx) error {
		if c.Method() != fiber.MethodGet && c.Method() != fiber.MethodHead {
			return c.Next()
		}
		p := c.Path()
		// Veri uçları statik sunucuya UĞRAMAZ.
		if strings.HasPrefix(p, "/api") || p == WSPath || strings.HasPrefix(p, WSPath+"/") {
			return c.Next()
		}
		// index.html herkese açıktır (giriş ekranı da statiktir).
		if rel, ok := s.staticTarget(root, p); ok {
			return sendFileUnlocked(c, rel)
		}
		// SPA rotası: uzantısız ve dosyası yoksa → index.html.
		if path.Ext(p) == "" {
			return sendFileUnlocked(c, filepath.Join(root, "index.html"))
		}
		return c.Next() // gerçek 404 (eksik .js/.css → HTML dönmek uygulamayı bozar)
	})
	s.log.Info("statik arayüz sunuluyor", "dizin", root)
}

// staticTarget, istek yolunu dizin içindeki dosyaya çevirir; güvenli değilse false döner.
//
// GÜVENLİK: ".." ile dizin DIŞINA çıkmak (path traversal) engellenir. Kullanıcı
// kontrollü bir yoldan /etc/passwd ya da server.key okuyamamalıdır.
func (s *Server) staticTarget(root, urlPath string) (string, bool) {
	clean := filepath.Clean("/" + strings.TrimPrefix(urlPath, "/"))
	full := filepath.Join(root, clean)
	// Join temizlediği için burada ".." kalmaz; yine de kök dışı kontrolü yapılır.
	if full != root && !strings.HasPrefix(full, root+string(filepath.Separator)) {
		return "", false
	}
	fi, err := os.Stat(full)
	if err != nil || fi.IsDir() {
		return "", false
	}
	return full, true
}

// sendFileUnlocked dosyayı okuyup gönderir ve TANITICIYI KAPATIR.
//
// Fiber'ın SendFile'ı dosyayı açık bırakır (Windows'ta build kilitlenir).
// Bu yüzden içerik belleğe alınır; dosya küçüktür (JS/CSS/HTML).
func sendFileUnlocked(c *fiber.Ctx, file string) error {
	body, err := os.ReadFile(file)
	if err != nil {
		return c.Next()
	}
	ct := fiber.MIMEApplicationJSONCharsetUTF8
	switch strings.ToLower(filepath.Ext(file)) {
	case ".html":
		ct = "text/html; charset=utf-8"
	case ".js", ".mjs":
		ct = "text/javascript; charset=utf-8"
	case ".css":
		ct = "text/css; charset=utf-8"
	case ".svg":
		ct = "image/svg+xml"
	case ".json":
		ct = fiber.MIMEApplicationJSON
	case ".png":
		ct = "image/png"
	case ".jpg", ".jpeg":
		ct = "image/jpeg"
	case ".ico":
		ct = "image/x-icon"
	case ".woff2":
		ct = "font/woff2"
	case ".woff":
		ct = "font/woff"
	case ".webmanifest":
		ct = "application/manifest+json"
	case ".txt":
		ct = "text/plain; charset=utf-8"
	}
	c.Set(fiber.HeaderContentType, ct)
	// Hashed varlıklar (üretimde adları hash içerir) uzun önbelleklenebilir.
	if strings.Contains(filepath.Base(file), ".") && c.Method() != fiber.MethodHead {
		c.Set(fiber.HeaderCacheControl, "public, max-age=3600")
	}
	c.Set(fiber.HeaderXContentTypeOptions, "nosniff")
	return c.Status(fiber.StatusOK).Send(body)
}

// App Fiber uygulamasını döndürür (test ve harici montaj için).
func (s *Server) App() *fiber.App { return s.app }

// Listen sunucuyu başlatır.
//
// TLSCertFile verilmişse HTTPS konuşur. TLS kullanılıyorsa sertifika/anahtar
// ÖNCEDEN doğrulanır: eksik veya bozuk dosya ile sunucu "başladı" deyip sonra
// düşmemelidir (canlı hata sınıfı — çalışıyor gibi görünmeyen sunucu).
//
// WebSocket yükseltmesi TLS üzerinde de çalışır; Fiber'ın wss:// destekleyen
// yolu kullanıldığı için istemcinin wss:// kullanması yeterlidir.
func (s *Server) Listen(addr string) error {
	if s.cfg.TLSCertFile == "" && s.cfg.TLSKeyFile == "" {
		return s.app.Listen(addr)
	}
	if s.cfg.TLSCertFile == "" || s.cfg.TLSKeyFile == "" {
		return errors.New("api: TLSCertFile ve TLSKeyFile BİRLİKTE verilmelidir")
	}
	// Önden doğrula: dosyalar yoksa/bozuksa anlaşılır hata ver.
	if _, err := tls.LoadX509KeyPair(s.cfg.TLSCertFile, s.cfg.TLSKeyFile); err != nil {
		return fmt.Errorf("api: TLS sertifikası yüklenemedi (%s, %s): %w",
			s.cfg.TLSCertFile, s.cfg.TLSKeyFile, err)
	}
	s.log.Info("HTTPS dinleniyor", "adres", addr, "sertifika", s.cfg.TLSCertFile)
	return s.app.ListenTLS(addr, s.cfg.TLSCertFile, s.cfg.TLSKeyFile)
}

// Shutdown açık HTTP bağlantılarını kapatır. Önce agent.Close ve hub.Close çağrılmalıdır.
func (s *Server) Shutdown(ctx context.Context) error { return s.app.ShutdownWithContext(ctx) }

func (s *Server) routes() {
	g := s.app.Group("/api")
	// /health BILerek KORUMASIZ: sürüm, uptime ve motor durumu döner ama veri
	// sızdırmaz; izleme (health probe) anahtarsız çalışabilmelidir.
	// /models ve /system da korumasızdır: ikisi de yalnızca YAPILANDIRMA bilgisi
	// döner, kullanıcı verisi değil. Erişilebilirlik için açık bırakıldı.
	g.Get("/health", s.health)

	// Hesap uçları. Giriş/kayıt KİMLİK DOĞRULAMAZ (kimse giremezse kimse üye de olunamaz);
	// üyelik kapalıyken 404 döner.
	// Hesap uçları KİMLİK DOĞRULAMAZ: giriş yapabilmek için zaten giriş yapamazsınız.
	// Bu yüzden bu uçlar korumalı gruptan ÖNCE ve KENDİ middleware'iyle tanımlanır.
	// Kullanıcıyı çözen katman (resolveUser) burada devreye girmez; giriş/kayıt
	// kendi token'ını üretir.
	app := s.app.Group("/api/auth")
	app.Post("/register", s.usersEnabled, s.registerUser)
	app.Post("/login", s.usersEnabled, s.loginUser)
	app.Post("/logout", s.usersEnabled, s.logoutUser)
	app.Get("/me", s.usersEnabled, s.meUser)
	g.Get("/models", s.listModels)
	g.Get("/system", s.systemStatus)

	// KORUMALI GRUP: aşağıdaki HER uç yetkilendirme gerektirir.
	// AuthKey boşken (yerel geliştirme) koruma pasif kalır; doluyken eksik/yanlış
	// X-Fiko-Auth başlığı 401 döner.
	g = g.Group("", s.authorize, s.resolveUser)

	g.Get("/conversations", s.listConversations)
	g.Post("/conversations", s.createConversation)
	g.Get("/conversations/:id", s.getConversation)
	g.Put("/conversations/:id", s.renameConversation)
	g.Delete("/conversations/:id", s.deleteConversation)
	g.Get("/conversations/:id/messages", s.listMessages)
	g.Get("/settings", s.getSettings)
	g.Put("/settings", s.putSettings)

	// Skill Registry. "/validate" sabit yoldur; "/:id" ile çakışmaz (farklı metot), yine de önce tanımlanır.
	sk := g.Group("/skills", s.skillsAvailable)
	sk.Get("/", s.listSkills)
	sk.Post("/", s.createSkill)
	sk.Post("/validate", s.validateSkill)
	sk.Post("/search", s.searchSkills)
	sk.Post("/reindex", s.reindexSkills)
	sk.Get("/:id", s.getSkill)
	sk.Put("/:id", s.updateSkill)
	sk.Delete("/:id", s.deleteSkill)
	sk.Put("/:id/status", s.setSkillStatus)
	sk.Post("/:id/duplicate", s.duplicateSkill)
	// Skill'i bağlı tool'u üzerinden çalıştır (yetki Executor'da denetlenir).
	sk.Post("/:id/execute", s.executeSkill)

	// Tool Registry (Faz 4). "/validate" ve "/:id" farklı yollar; sabit yollar önce tanımlanır.
	tl := g.Group("/tools", s.toolsAvailable)
	tl.Get("/", s.listTools)
	tl.Post("/", s.createTool)
	tl.Post("/validate", s.validateTool)
	tl.Get("/:id", s.getTool)
	tl.Put("/:id", s.updateTool)
	tl.Delete("/:id", s.deleteTool)
	tl.Put("/:id/status", s.setToolStatus)
	tl.Post("/:id/execute", s.executeTool)

	// Permission sistemi: kullanıcının tool yetkileri.
	g.Get("/permissions", s.getPermissions)
	g.Put("/permissions", s.putPermissions)

	// Üyenin kendi sayfası: yetkisi + davranışı, SALT OKUNUR (düzenleme yalnız yöneticide).
	g.Get("/me/page", s.usersEnabled, s.getMyPage)

	// Yönetici uçları: üyeler ve onların tool politikaları. Üyelik kapalıyken 404, yönetici
	// olmayana da 404 (uç görünmez). Servis kontrolü rol kontrolünden SONRA gelir.
	adm := g.Group("/admin", s.usersEnabled, s.adminOnly, s.adminAvailable)
	adm.Get("/users", s.adminListUsers)
	adm.Put("/users/:id/permissions", s.adminSetPermissions)
	// Sistem istemi parçaları: yalnızca yönetici görür ve düzenler.
	adm.Get("/prompts", s.getPrompts)
	adm.Put("/prompts", s.putPrompts)
	// Kullanıcı davranış profilleri (persona): yalnızca yönetici yönetir.
	//
	// personasAvailable, özellik kapalıysa (servis yok) 503 döner: 404 DEĞİL,
	// çünkü yol mevcut — kayıt yok demek yanlış sinyal olurdu.
	pa := adm.Group("/personas", s.personasAvailable)
	pa.Get("", s.listPersonas)
	pa.Post("", s.createPersona)
	pa.Put("/:id", s.updatePersona)
	pa.Delete("/:id", s.deletePersona)

	// Bilgi Tabanı / RAG (Faz 8). Belgeler kullanıcıya aittir; sabit yollar ("/search", "/reindex") önce.
	kb := g.Group("/knowledge", s.knowledgeAvailable)
	kb.Post("/search", s.searchKnowledge)
	kb.Post("/reindex", s.reindexKnowledge)
	kb.Get("/documents", s.listKnowledgeDocs)
	kb.Post("/documents", s.createKnowledgeDoc)
	kb.Get("/documents/:id", s.getKnowledgeDoc)
	kb.Get("/documents/:id/chunks", s.listKnowledgeChunks)
	kb.Delete("/documents/:id", s.deleteKnowledgeDoc)

	// Kullanicilar arasi bildirim: gelen/gonen kutusu, okundu, yanit, silme.
	// GUVENLIK: bildirimler ALICIYA aittir; her uc currentUser ile calisir.
	nt := g.Group("/notifications", s.notifyAvailable)
	nt.Get("/", s.listNotifications)
	nt.Get("/:id", s.getNotification)
	nt.Post("/:id/read", s.readNotification)
	nt.Post("/:id/answer", s.answerNotification)
	nt.Delete("/:id", s.deleteNotification)

	// Agent Builder / Akışlar (Faz 9). Akışlar ve yürütmeler kullanıcıya aittir.
	// SABİT yollar (runs, meta, validate) "/:id" ile çakıştığı için ÖNCE tanımlanır:
	// Fiber ilk eşleşeniği kullanır, "/runs" aksi hâlde :id sanılır.
	wf := g.Group("/workflows", s.workflowsAvailable)
	wf.Get("/meta", s.workflowMeta)
	wf.Post("/validate", s.validateWorkflow)
	wf.Get("/runs", s.listExecutions)
	wf.Get("/runs/:id", s.getExecution)
	wf.Post("/runs/:id/cancel", s.cancelExecution)
	wf.Get("/", s.listWorkflows)
	wf.Post("/", s.createWorkflow)
	wf.Get("/:id", s.getWorkflow)
	wf.Put("/:id", s.updateWorkflow)
	wf.Delete("/:id", s.deleteWorkflow)
	wf.Post("/:id/duplicate", s.duplicateWorkflow)
	wf.Post("/:id/run", s.runWorkflow)

	// Kalıcı bellek (Faz 10). Anılar kullanıcıya aittir. SABİT yollar ("/search", "/reindex") "/:id" ile
	// çakışmasın diye ÖNCE tanımlanır. DELETE "/" tüm anıları siler (?confirm=true ister).
	mem := g.Group("/memories", s.memoryAvailable)
	mem.Post("/search", s.searchMemories)
	mem.Post("/reindex", s.reindexMemories)
	mem.Get("/", s.listMemories)
	mem.Post("/", s.createMemory)
	mem.Delete("/", s.deleteAllMemories)
	mem.Get("/:id", s.getMemory)
	mem.Put("/:id", s.updateMemory)
	mem.Delete("/:id", s.deleteMemory)
}

// middleware istek işleme zincirini kurar: sıra önemlidir (recoverer en dışta, CORS en içte).
// Böylece CORS ön kontrolü (OPTIONS) de loglanır ve kullanıcı kimliği WS/REST'te aynı çözülür.
func (s *Server) middleware() {
	s.app.Use(s.recoverer)
	s.app.Use(s.requestLogger)
	s.app.Use(s.resolveUser)
	origins := "*"
	if len(s.cfg.AllowedOrigins) > 0 {
		origins = strings.Join(s.cfg.AllowedOrigins, ",")
	}
	s.app.Use(cors.New(cors.Config{
		AllowOrigins: origins,
		AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
		// AllowHeaders'te AuthHeader OLMAK ZORUNDA: tarayıcı giriş başlığını
		// göndermeden önce preflight (OPTIONS) yapar ve izin listesinde yoksa
		// isteği ENGELLER. Bu alan eksikken giriş ekranı anahtarı gönderemez
		// ve her istek CORS hatasıyla düşer (canlı bulgu).
		AllowHeaders:  "Origin,Content-Type,Accept,Authorization," + UserHeader + "," + AuthHeader,
		ExposeHeaders: "Content-Length",
		MaxAge:        3600,
	}))
}
