package user

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"strings"
	"sync"
	"time"

	"golang.org/x/crypto/bcrypt"
)

// defaultSessionTTL oturum ömrü: kullanıcı giriş yapar, makul bir süre sonra
// yeniden giriş yapar. 30 gün.
const defaultSessionTTL = 30 * 24 * time.Hour

// Session, bir oturumun kaydıdır.
//
// Token YALNIZCA istemciye verilir; sunucuda HASH'i saklanır. Böylece veritabanı
// sızarsa geçerli oturum token'ları ele geçirilemez (parola hash'le aynı mantık).
type Session struct {
	ID        string
	UserID    string
	TokenHash string
	CreatedAt time.Time
	ExpiresAt time.Time
}

// Service, hesap iş kurallarını uygular: kayıt, giriş doğrulama, oturum yönetimi.
//
// Parola özetleme burada, deponun UYGULANMASINDA değil: saklama katmanı hash'i
// kıyaslayacak bilgiye ihtiyaç duymaz.
type Service struct {
	store      Store
	ttl        time.Duration
	bcryptCost int

	mu       sync.RWMutex
	sessions map[string]Session // anahtar: token özeti (token'ın kendisi değil)
}

// ServiceOptions, servis yapılandırmasıdır.
type ServiceOptions struct {
	// TTL oturum ömrü; <=0 ise varsayılan (30 gün).
	TTL time.Duration
	// BcryptCost 4-31 arası; <=0 ise 10 (makul bir güvenlik/hız dengesi).
	BcryptCost int
}

// NewService hesap servisini üretir.
func NewService(store Store, opts ServiceOptions) (*Service, error) {
	if store == nil {
		return nil, errors.New("depo zorunludur")
	}
	if opts.TTL <= 0 {
		opts.TTL = defaultSessionTTL
	}
	if opts.BcryptCost <= 0 {
		opts.BcryptCost = bcrypt.DefaultCost
	}
	if opts.BcryptCost < bcrypt.MinCost || opts.BcryptCost > bcrypt.MaxCost {
		return nil, errors.New("geçersiz bcrypt maliyeti")
	}
	return &Service{
		store:      store,
		ttl:        opts.TTL,
		bcryptCost: opts.BcryptCost,
		sessions:   map[string]Session{},
	}, nil
}

// hashPassword, parolanın bcrypt özetini üretir.
func (s *Service) hashPassword(plain string) (string, error) {
	b, err := bcrypt.GenerateFromPassword([]byte(plain), s.bcryptCost)
	if err != nil {
		return "", err
	}
	return string(b), nil
}

// ErrInvalidCredentials, giriş başarısızlığının TEK hata mesajıdır.
//
// Neden tek mesaj? "kullanıcı yok" ile "parola yanlış" ayrı ayrı dönerse saldırgan
// hangi kullanıcı adlarının VAR olduğunu öğrenir (kullanıcı adı sızdırma).
var ErrInvalidCredentials = errors.New("kullanıcı adı veya parola hatalı")

// dummyHash, "kullanıcı yok" yolunda süreyi dengelemek için kullanılır.
// Geçerli bir bcrypt özeti (bcrypt, "$2a$" ile başlar).
var dummyHash = []byte("$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy")

// Authenticate parolayı doğrular ve kullanıcıyı döner.
//
// ZAMANLAMA DENGESİ: kullanıcı yoksa da bir bcrypt işlemi yapılır. Aksi hâlde
// "bilinmeyen kullanıcı" anında gelir, "var ama parola yanlış" yavaş gelir —
// bu fark kullanıcı adı varlığını sızdırır.
func (s *Service) Authenticate(ctx context.Context, username, password string) (*User, error) {
	u, err := s.store.ByUsername(ctx, username)
	if err != nil {
		_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(password))
		return nil, ErrInvalidCredentials
	}
	if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
		return nil, ErrInvalidCredentials
	}
	if u.Disabled {
		return nil, ErrDisabled
	}
	return u, nil
}

// Register yeni hesap açar.
func (s *Service) Register(ctx context.Context, username, password, displayName string) (*User, error) {
	if err := ValidUsername(username); err != nil {
		return nil, err
	}
	if err := ValidPassword(password); err != nil {
		return nil, err
	}
	// KURULUM: hiç hesap yokken açılan İLK hesap yönetici olur; sonrakiler normal üyedir.
	// Aksi hâlde .env'e FIKO_BOOTSTRAP_PASSWORD yazmadan hiçbir yönetici olamaz ve üyelere
	// araç yetkisi verilemezdi. Dikkat: sunucu ilk kez açılıp kimse kayıt olmadan internete
	// açılırsa ilk gelen yönetici olur; önce kendi hesabınızı açın (veya BOOTSTRAP kullanın).
	role := RoleUser
	if existing, lerr := s.store.List(ctx, 1); lerr == nil && len(existing) == 0 {
		role = RoleAdmin
	}
	u, err := New(username, displayName, role)
	if err != nil {
		return nil, err
	}
	hash, err := s.hashPassword(password)
	if err != nil {
		return nil, err
	}
	u.PasswordHash = hash
	if err := s.store.Create(ctx, u); err != nil {
		return nil, err
	}
	return u, nil
}

// Login giriş doğrular ve yeni oturum açar.
func (s *Service) Login(ctx context.Context, username, password string) (*User, string, time.Time, error) {
	u, err := s.Authenticate(ctx, username, password)
	if err != nil {
		return nil, "", time.Time{}, err
	}
	token, expires, err := s.newSession(u.ID)
	if err != nil {
		return nil, "", time.Time{}, err
	}
	_ = s.store.UpdateLastLogin(ctx, u.ID, time.Now().Unix())
	return u, token, expires, nil
}

// newSession rastgele bir token üretir ve yalnızca HASH'ini saklar.
//
// Token 32 bayt kriptografik rastgelelikten gelir (base64url). Veritabanı okunsa
// bile oturum çalınması engellenir.
func (s *Service) newSession(userID string) (string, time.Time, error) {
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", time.Time{}, err
	}
	raw := base64.RawURLEncoding.EncodeToString(buf)
	now := time.Now().UTC()
	expires := now.Add(s.ttl)

	s.mu.Lock()
	s.sessions[hashToken(raw)] = Session{
		ID:        newSessionID(),
		UserID:    userID,
		TokenHash: hashToken(raw),
		CreatedAt: now,
		ExpiresAt: expires,
	}
	s.mu.Unlock()
	return raw, expires, nil
}

// hashToken, token'ın SHA-256 özetidir. Token zaten yüksek entropili olduğu
// için bcrypt gereksiz; SHA-256 hem hızlı hem çakışmasızdır.
func hashToken(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}

// Validate, oturum token'ını doğrular ve kullanıcıyı döner.
func (s *Service) Validate(ctx context.Context, token string) (*User, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return nil, ErrInvalidCredentials
	}
	s.mu.RLock()
	sess, ok := s.sessions[hashToken(token)]
	s.mu.RUnlock()
	if !ok {
		return nil, ErrInvalidCredentials
	}
	if time.Now().UTC().After(sess.ExpiresAt) {
		s.Logout(token) // süresi dolmuş oturumu temizle
		return nil, ErrInvalidCredentials
	}
	u, err := s.store.ByID(ctx, sess.UserID)
	if err != nil {
		return nil, ErrInvalidCredentials
	}
	if u.Disabled {
		return nil, ErrDisabled
	}
	return u, nil
}

// Logout oturumu sonlandırır (token geçersizleşir).
func (s *Service) Logout(token string) {
	s.mu.Lock()
	delete(s.sessions, hashToken(strings.TrimSpace(token)))
	s.mu.Unlock()
}

// LogoutAll bir kullanıcının TÜM oturumlarını kapatır (parola değişimi veya
// "her cihazdan çıkış" için).
func (s *Service) LogoutAll(userID string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	for k, v := range s.sessions {
		if v.UserID == userID {
			delete(s.sessions, k)
		}
	}
}

// ActiveSessions, kullanıcının açık oturum sayısını döner.
func (s *Service) ActiveSessions(userID string) int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	n := 0
	now := time.Now().UTC()
	for _, v := range s.sessions {
		if v.UserID == userID && now.Before(v.ExpiresAt) {
			n++
		}
	}
	return n
}

// ByID kullanıcıyı kimlikle döner.
func (s *Service) ByID(ctx context.Context, id string) (*User, error) {
	return s.store.ByID(ctx, id)
}

// List hesapları listeler.
func (s *Service) List(ctx context.Context, limit int) ([]*User, error) {
	return s.store.List(ctx, limit)
}

// EnsureBootstrap, verilen kullanıcı yoksa yönetici olarak oluşturur (ilk kurulum).
//
// Parola ortam değişkeninden gelir; VERİLMEZSE HESAP OLUŞTURULMAZ. Rastgele bir
// parola üretip saklamak, kullanıcıya "giriş yapamıyorum" hatasından daha kötüdür.
func (s *Service) EnsureBootstrap(ctx context.Context, username, password string) (*User, error) {
	if strings.TrimSpace(password) == "" {
		return nil, errors.New("ilk kurulum parolası verilmedi")
	}
	if existing, err := s.store.ByUsername(ctx, username); err == nil {
		return existing, nil
	}
	u, err := New(username, "", RoleAdmin)
	if err != nil {
		return nil, err
	}
	hash, err := s.hashPassword(password)
	if err != nil {
		return nil, err
	}
	u.PasswordHash = hash
	if err := s.store.Create(ctx, u); err != nil {
		return nil, err
	}
	return u, nil
}

// unixTime, Unix saniyesini time.Time'a çevirir (depo imzasını korumak için).
func unixTime(sec int64) time.Time {
	if sec == 0 {
		return time.Time{}
	}
	return time.Unix(sec, 0).UTC()
}

// newSessionID, oturum için benzersiz bir kimlik üretir.
func newSessionID() string {
	buf := make([]byte, 9)
	if _, err := rand.Read(buf); err != nil {
		return ""
	}
	return "ses_" + hex.EncodeToString(buf)
}
