package api

import (
	"crypto/subtle"
	"errors"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"

	"fiko-ai/internal/protocol"
)

// localsUserID Fiber Locals içinde kullanıcı kimliğinin saklandığı anahtardır.
const localsUserID = "fiko.userID"

// UserHeader istemcinin kullanıcı kimliğini bildirdiği başlıktır.
//
// GUVENLİK: bu başlık KENDİ BAŞINA kimlik SAYILMAZ. AuthKey tanımlıysa
// istek önce yetkilendirilir; aksi hâlde istemci istediği kullanıcıyı taklit eder
// (canlı bulgu: "hacker" kimliğiyle konuşma oluşturulabildi).
const UserHeader = "X-Fiko-User"

// AuthHeader, yetkilendirme anahtarının taşındığı başlıktır.
const AuthHeader = "X-Fiko-Auth"

// resolveUserID isteğin kullanıcısını belirler ve Locals'a yazar.
//
// ÖNCELİK SIRASI (güvenlik açısından önemli):
//
//  1. Oturum token'ı (Authorization: Bearer veya fiko_session çerezi)
//     → GERÇEK kimlik doğrulaması. Kullanıcı adını SUNUCU belirler.
//  2. FIKO_API_KEY (X-Fiko-Auth) → TEK paylaşılan anahtar (geliştirme/servis).
//  3. X-Fiko-User başlığı → YALNIZCA üyelik sistemi kapalıyken kabul edilir.
//
// Neden 3. adım koşullu? Çünkü başlık istemciye açıktır: "hacker" yazarak
// başkasının kutusuna girilebilirdi. Üyelik AÇIKKEN bu başlığa güvenilmez.
func (s *Server) resolveUserID(c *fiber.Ctx) (string, error) {
	// Fiber ctx havuzu Locals'ı bir sonraki isteğe taşıyabilir: her çözümlemede sıfırla.
	c.Locals(localsUserID, nil)
	c.Locals(localsUserRole, nil)
	c.Locals(localsViaSession, nil)

	// 1) Oturum token'ı. HER yolda denenir (kimlik uçları dahil): /api/auth/me ve /logout
	// oturumu çözebilmelidir; önceden bu yollar erken döndüğü için /me HER ZAMAN 401 veriyordu.
	if s.deps.Users != nil {
		if tok := bearerToken(c); tok != "" {
			u, err := s.deps.Users.Validate(c.Context(), tok)
			if err == nil {
				c.Locals(localsUserID, u.ID)
				c.Locals(localsUserRole, string(u.Role))
				c.Locals(localsViaSession, true)
				return u.ID, nil
			}
			// Token var ama geçersiz: korumalı uçta sessizce devam ETME. Açık uçlarda ise bayat
			// bir token yüzünden kilitlenme: ön yüz /api/system'i okuyup giriş ekranını açabilsin.
			if !isPublicRequest(c) {
				return "", errBadSession
			}
		}
	}

	// Üyelik açıkken açık uçlar anonim geçer (kimlik gerektirmezler).
	if s.cfg.UsersEnabled && isPublicRequest(c) {
		return s.cfg.DefaultUserID, nil
	}

	// 2) Paylaşılan API anahtarı (üyelik kapalıyken de çalışır)
	if strings.TrimSpace(s.cfg.AuthKey) != "" && secureEqual(c.Get(AuthHeader), strings.TrimSpace(s.cfg.AuthKey)) {
		id := strings.Clone(strings.TrimSpace(s.cfg.DefaultUserID))
		c.Locals(localsUserID, id)
		c.Locals(localsUserRole, roleAdmin) // işletmecinin sırrı → yönetici
		return id, nil
	}

	// 3) Geliştirme başlığı — YALNIZCA üyelik sistemi kapalıyken
	if !s.cfg.UsersEnabled {
		id := strings.Clone(strings.TrimSpace(c.Get(UserHeader)))
		if id == "" {
			id = s.cfg.DefaultUserID
		}
		if len(id) > 128 {
			return "", errors.New("kullanıcı kimliği çok uzun")
		}
		c.Locals(localsUserID, id)
		return id, nil
	}

	// Üyelik açık ama oturum yok.
	return "", errLoginRequired
}

// resolveUser middleware sarmalayıcısıdır; kimlik çözülemezse istek reddedilir.
//
// İSTİSNA: statik arayüz dosyaları (HTML/JS/CSS/politika) kimlik arkasında DEĞİLDİR
// (canlı 05.10.2026 — HTTPS yayını).
//   Neden? Giriş ekranı da bir statik dosyadır. Arayüzün kendisi 401 alırsa kullanıcı
//   giriş EKRANINI hiç göremez ve "site açılmıyor" sanır; tarayıcı hata JSON'unu
//   HTML sanar. Üstelik kimlik denetimi zaten /api ve /ws'te var — SPA yalnız kabuk
//   dosyalardır, veri içermez.
//   Kural: YÖNLENDİRME (uzantısız) istekler de index.html'dir ve yine açıktır;
//   API/Ws hiçbir koşulda bu istisnadan yararlanmaz.
func (s *Server) resolveUser(c *fiber.Ctx) error {
	// CORS ön kontrolü (OPTIONS) kimlik taşıyamaz: tarayıcı başlıkları ön kontrolde göndermez.
	if c.Method() == fiber.MethodOptions {
		return c.Next()
	}
	// İstisna YALNIZCA statik arayüz yayınlandığında geçerlidir.
	if s.servesStatic() && isStaticAsset(c) {
		return c.Next()
	}
	if _, err := s.resolveUserID(c); err != nil {
		status, code := authFailStatus(err)
		return fail(c, status, code, err.Error())
	}
	return c.Next()
}

// servesStatic, gerçekten statik arayüz sunulup sunulmadığını söyler.
//
// Neden şart? İstisna (statik dosyalar kimlik arkasında değil) YALNIZCA arayüzün
// gerçekten sunulduğu durumda anlamlıdır. HTTP_STATIC_DIR boşken (geliştirme)
// "her şey statiktir" demek KİMLİĞİ TAMAMEN KALDIRIRDI: /api de istisnadan
// yararlanırdı. Bu yüzden kapı önce bu koşula bağlanır.
func (s *Server) servesStatic() bool { return strings.TrimSpace(s.cfg.StaticDir) != "" }

// isStaticAsset, isteğin kimlik gerektirmeyen statik varlık olduğunu söyler.
//
// /api ve /ws KAPSAM DIŞIDIR: onlar veri uçlarıdır ve her zaman kimlik ister.
func isStaticAsset(c *fiber.Ctx) bool {
	p := c.Path()
	if strings.HasPrefix(p, "/api") || p == WSPath || strings.HasPrefix(p, WSPath+"/") {
		return false
	}
	// SPA kabuğu: /, /ayarlar, /tools … (uzantısız) → index.html
	return true
}

// currentUser isteğin kullanıcısını döndürür (resolveUser çalıştıysa kesinlikle doludur).
func (s *Server) currentUser(c *fiber.Ctx) string {
	if id, _ := c.Locals(localsUserID).(string); id != "" {
		return id
	}
	return s.cfg.DefaultUserID
}

// recoverer handler paniklerini yakalar ve 500 gövdesi döner; süreç yaşamaya devam eder.
func (s *Server) recoverer(c *fiber.Ctx) (err error) {
	defer func() {
		if r := recover(); r != nil {
			s.log.Error("http handler panic",
				"method", c.Method(), "path", c.Path(), "panic", r, "user", s.currentUser(c))
			if e := c.Status(fiber.StatusInternalServerError).JSON(errorBody{
				Error: apiError{Code: protocol.ErrCodeInternal, Message: "beklenmeyen sunucu hatası"},
			}); e != nil {
				err = e
			}
		}
	}()
	return c.Next()
}

// requestLogger her istek için süre, durum ve boyut kaydeder. 5xx error, 4xx warn seviyesindedir.
func (s *Server) requestLogger(c *fiber.Ctx) error {
	start := time.Now()
	err := c.Next()
	status := c.Response().StatusCode()
	if err != nil {
		// Hata gövdesi Fiber'in ErrorHandler'ında yazılır; burada yalnızca kaydedilir.
		var fe *fiber.Error
		if errors.As(err, &fe) {
			status = fe.Code
		} else {
			status = fiber.StatusInternalServerError
		}
	}
	s.logRequest(c, status, time.Since(start))
	return err
}

func (s *Server) logRequest(c *fiber.Ctx, status int, took time.Duration) {
	if status == fiber.StatusNotFound || status == 0 {
		// 404 ve 0 (yazma hatası) gürültü yaratmasın: yalnızca debug.
		s.log.Debug("http", "method", c.Method(), "path", c.Path(), "status", status, "ms", took.Milliseconds())
		return
	}
	attrs := []any{
		"method", c.Method(), "path", c.Path(), "status", status,
		"ms", took.Milliseconds(), "bytes", len(c.Response().Body()),
		"user", s.currentUser(c),
	}
	switch {
	case status >= 500:
		s.log.Error("http", attrs...)
	case status >= 400:
		s.log.Warn("http", attrs...)
	default:
		s.log.Info("http", attrs...)
	}
}

// authorize, AuthKey tanimli ise X-Fiko-Auth basligini DOGRULAR.
//
// Neden ayri katman? resolveUserID kimligi COZER (hangi kullanici), authorize
// yetkilendirir (bu istek yapilabilir mi). Kimlik dogrulanmadan kullanici adi
// guvenilmez; kullanici adi dogrulanmadan erisim verilemez.
//
// GUELTLER:
//   - AuthKey bos ise dogrulama YAPILMAZ (yerel gelistirme akisi bozulmaz).
//     Bu durumda sistem acik KABUL edilir ve /api/system bunu bildirir.
//   - AuthKey tanimliysa eksik/yanlis anahtar 401 doner.
//   - Anahtarlar sabit zamanli karsilastirilir (timing attack azaltma).
func (s *Server) authorize(c *fiber.Ctx) error {
	// Giriş/üyelik uçları koruma GEREKTİRMEZ: kim girecekse giriş yapamaz,
	// kimse yeni üye olamaz. Fiber grup middleware'i tüm uçlara uyguladığı için
	// istisna burada yapılır.
	if isAuthPath(c) {
		return c.Next()
	}
	// Geçerli bir OTURUM token'ı paylaşılan API anahtarının yerini tutar: üye zaten parolayla
	// doğrulandı. (Önceden anahtar doluyken oturumlu üyeye de X-Fiko-Auth isteniyordu.)
	if viaSession(c) {
		return c.Next()
	}
	key := strings.TrimSpace(s.cfg.AuthKey)
	if key == "" {
		return c.Next() // kimlik dogrulama kapali (gelistirme varsayilani)
	}
	// Boşluk KIRPILMAZ: anahtar tam olarak eşleşmelidir. Kırpmak, yazım hatasıyla
	if !secureEqual(c.Get(AuthHeader), key) {
		return fail(c, fiber.StatusUnauthorized, protocol.ErrCodeUnauthorized,
			"yetkilendirme gerekli: "+AuthHeader+" başlığı eksik veya hatalı")
	}
	return c.Next()
}

// secureEqual, iki anahtarı sabit zamanlı karşılaştırır.
//
// Neden crypto/subtle? string == ile karşılaştırma, eşleşen bayt sayısına kadar
// farklı süre harcar ve hatalı anahtarı kademeli tahmin etmeye çalışan saldırıya
// zaman sinyali verir.
func secureEqual(got, want string) bool {
	if got == "" || want == "" {
		return false
	}
	// Uzunluk farkı zaman sızıntısı üretmesin diye önce eşit uzunlukta kıyasla.
	if len(got) != len(want) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(got), []byte(want)) == 1
}

// checkAuth, yetkilendirme anahtarını DOĞRULAR ve sonucu bool döner.
//
// WS için de kullanılır. Tarayıcı WebSocket API'si özel başlık gönderemediği
// için anahtar "?key=" sorgu parametresiyle de taşınabilir.
//
// DİKKAT: anahtarı URL'ye yazmak sunucu günlüklerine (access log) sızabilir.
// Bu yüzden kullanıcıya önce başlık yöntemi önerilir; sorgu parametresi yalnızca
// tarayıcı WS kısıtı için vardır.
func (s *Server) checkAuth(c *fiber.Ctx) bool {
	// Geçerli oturum token'ı (Bearer, çerez veya /ws?token=) anahtarın yerini tutar.
	if _, ok := s.sessionUser(c); ok {
		return true
	}
	key := strings.TrimSpace(s.cfg.AuthKey)
	if key == "" {
		return true // doğrulama kapalı (geliştirme)
	}
	if secureEqual(c.Get(AuthHeader), key) {
		return true
	}
	return secureEqual(c.Query("key"), key)
}

// isAuthPath, istek yolunun kimlik uçlarına ait olup olmadığını söyler.
//
// Neden gerekli? Fiber'da grup middleware'i (authorize, resolveUser) gruptan ÖNCE
// tanımlanmış uçlara da uygulanır. Bu istisna olmadan /api/auth/login kendi
// "giriş gerekli" hatasını üretir ve kimse giremez.
func isAuthPath(c *fiber.Ctx) bool {
	return strings.HasPrefix(c.Path(), "/api/auth/")
}
