// Handlers d'authentification : login PAM, logout, refresh token, profil utilisateur.
package api

import (
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"fmt"
	"log"
	"net/http"
	"time"

	"git.geronzi.fr/proxmoxPanel/core/backend/internal/audit"
	"git.geronzi.fr/proxmoxPanel/core/backend/internal/auth"
	"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
	"github.com/go-chi/chi/v5"
)

// AuthHandler contient les handlers d'authentification.
type AuthHandler struct {
	db          *db.DB
	jwtManager  *auth.JWTManager
	sshAuth     *auth.SSHAuthenticator
	auditLogger *audit.Logger
	authLimiter *RateLimiter
}

// NewAuthHandler crée un AuthHandler.
func NewAuthHandler(database *db.DB, jwtMgr *auth.JWTManager, sshAuth *auth.SSHAuthenticator, auditLog *audit.Logger) *AuthHandler {
	return &AuthHandler{
		db:          database,
		jwtManager:  jwtMgr,
		sshAuth:     sshAuth,
		auditLogger: auditLog,
		authLimiter: NewRateLimiter(5, time.Minute), // 5 tentatives par minute par IP
	}
}

// Login authentifie un utilisateur via ses credentials Linux (PAM via SSH).
// POST /api/auth/login
// Body: { "username": "enzo", "password": "..." }
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
	ip := clientIP(r)

	// Rate limiting sur le login
	if !h.authLimiter.Allow(ip) {
		h.auditLogger.Log(nil, "?", "login_rate_limited", "", nil, ip)
		JSONError(w, "Trop de tentatives de connexion, veuillez patienter", http.StatusTooManyRequests)
		return
	}

	var body struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	if err := decodeJSON(r, &body); err != nil {
		JSONError(w, "Corps de requête invalide", http.StatusBadRequest)
		return
	}

	if body.Username == "" || body.Password == "" {
		JSONError(w, "Nom d'utilisateur et mot de passe requis", http.StatusBadRequest)
		return
	}

	// Lire le host SSH depuis la DB à chaque login.
	// L'authenticator global est créé au démarrage avec host="" (avant installation)
	// et ne se met pas à jour automatiquement après configuration.
	sshHost, _, _ := h.db.GetSetting("ssh_host")
	if sshHost == "" {
		log.Printf("[auth/login] SSH non configuré (ssh_host vide en base) — user=%s ip=%s", body.Username, ip)
		JSONError(w, "SSH non configuré, veuillez vérifier l'installation", http.StatusServiceUnavailable)
		return
	}

	log.Printf("[auth/login] Tentative — user=%s ip=%s ssh_host=%s", body.Username, ip, sshHost)
	authenticator := auth.NewSSHAuthenticator(sshHost)

	userInfo, err := authenticator.Authenticate(body.Username, body.Password)
	if err != nil {
		log.Printf("[auth/login] Échec auth SSH — user=%s ssh_host=%s erreur=%v", body.Username, sshHost, err)
		h.auditLogger.Log(nil, body.Username, "login_failed", "", map[string]string{"error": err.Error()}, ip)
		JSONError(w, "Identifiants invalides", http.StatusUnauthorized)
		return
	}
	log.Printf("[auth/login] Succès — user=%s admin=%v ssh_host=%s", body.Username, userInfo.IsAdmin, sshHost)

	// Créer ou mettre à jour le profil utilisateur en SQLite
	userID, err := h.upsertUser(userInfo)
	if err != nil {
		JSONError(w, "Erreur création profil utilisateur", http.StatusInternalServerError)
		return
	}

	// Générer les tokens JWT
	accessToken, err := h.jwtManager.GenerateAccessToken(userID, userInfo.Username, userInfo.IsAdmin)
	if err != nil {
		JSONError(w, "Erreur génération token", http.StatusInternalServerError)
		return
	}

	refreshToken, err := h.jwtManager.GenerateRefreshToken(userID)
	if err != nil {
		JSONError(w, "Erreur génération refresh token", http.StatusInternalServerError)
		return
	}

	// Stocker le hash du refresh token en base pour permettre la révocation
	tokenHash := hashToken(refreshToken)
	expiry := time.Now().Add(auth.RefreshTokenDuration())
	if _, err := h.db.Exec(`
		INSERT INTO refresh_tokens (user_id, token_hash, expires_at, user_agent, ip, last_used_at)
		VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
	`, userID, tokenHash, expiry, r.UserAgent(), clientIP(r)); err != nil {
		log.Printf("[auth/login] ERREUR stockage refresh token — user=%s userID=%d err=%v", body.Username, userID, err)
		JSONError(w, "Erreur création session", http.StatusInternalServerError)
		return
	}

	// Mettre à jour la date de dernier login
	h.db.Exec(`UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?`, userID)

	// Cookie httpOnly pour le refresh token
	// Secure=true si TLS direct ou si derrière un proxy (Traefik) qui a terminé TLS
	isHTTPS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
	// Path élargi à /api/auth/ pour que le cookie soit envoyé au logout aussi
	http.SetCookie(w, &http.Cookie{
		Name:     "pxp_refresh",
		Value:    refreshToken,
		Path:     "/api/auth/",
		HttpOnly: true,
		Secure:   isHTTPS,
		SameSite: http.SameSiteStrictMode,
		Expires:  expiry,
	})

	h.auditLogger.Log(&userID, userInfo.Username, "login_success", "", nil, ip)

	JSONResponse(w, http.StatusOK, map[string]any{
		"access_token": accessToken,
		"expires_in":   900, // 15 minutes en secondes
		"user": map[string]any{
			"id":       userID,
			"username": userInfo.Username,
			"is_admin": userInfo.IsAdmin,
		},
	})
}

// Logout invalide la session courante de l'utilisateur.
// POST /api/auth/logout
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
	claims := GetClaims(r)

	if claims != nil {
		// Supprimer uniquement le token de CETTE session (via cookie pxp_refresh)
		// Le cookie a Path=/api/auth/ donc il est bien envoyé sur ce endpoint.
		if cookie, err := r.Cookie("pxp_refresh"); err == nil {
			tokenHash := hashToken(cookie.Value)
			h.db.Exec(`DELETE FROM refresh_tokens WHERE token_hash = ? AND user_id = ?`,
				tokenHash, claims.UserID)
		} else {
			// Pas de cookie (session dégradée ou ancien cookie path) → supprimer toutes les sessions
			h.db.Exec(`DELETE FROM refresh_tokens WHERE user_id = ?`, claims.UserID)
		}
		h.auditLogger.Log(&claims.UserID, claims.Username, "logout", "", nil, clientIP(r))
	}

	// Effacer le cookie de refresh
	http.SetCookie(w, &http.Cookie{
		Name:     "pxp_refresh",
		Value:    "",
		Path:     "/api/auth/",
		HttpOnly: true,
		Expires:  time.Unix(0, 0),
		MaxAge:   -1,
	})

	JSONResponse(w, http.StatusOK, map[string]string{"message": "Déconnexion réussie"})
}

// Refresh renouvelle l'access token via le refresh token (cookie httpOnly).
// POST /api/auth/refresh
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
	ip := clientIP(r)

	cookie, err := r.Cookie("pxp_refresh")
	if err != nil {
		log.Printf("[auth/refresh] cookie absent — ip=%s err=%v", ip, err)
		JSONError(w, "Refresh token manquant", http.StatusUnauthorized)
		return
	}

	userID, err := h.jwtManager.ValidateRefreshToken(cookie.Value)
	if err != nil {
		log.Printf("[auth/refresh] JWT invalide — ip=%s err=%v", ip, err)
		JSONError(w, "Refresh token invalide ou expiré", http.StatusUnauthorized)
		return
	}

	// Vérifier que le token est en base (non révoqué)
	tokenHash := hashToken(cookie.Value)
	var count int
	h.db.QueryRow(`SELECT COUNT(*) FROM refresh_tokens WHERE user_id = ? AND token_hash = ? AND expires_at > CURRENT_TIMESTAMP`, userID, tokenHash).Scan(&count)
	if count == 0 {
		// Diagnostic : vérifier si le token existe mais avec un mauvais user_id
		var anyCount int
		h.db.QueryRow(`SELECT COUNT(*) FROM refresh_tokens WHERE token_hash = ?`, tokenHash).Scan(&anyCount)
		log.Printf("[auth/refresh] token non trouvé en base — userID=%d tokenHash=%s anyMatch=%d ip=%s",
			userID, tokenHash[:8], anyCount, ip)
		JSONError(w, "Session expirée ou révoquée", http.StatusUnauthorized)
		return
	}

	log.Printf("[auth/refresh] token valide — userID=%d ip=%s", userID, ip)

	// Mettre à jour la date de dernière utilisation
	h.db.Exec(`UPDATE refresh_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE token_hash = ?`, tokenHash)

	// Récupérer les infos utilisateur
	var username string
	var isAdmin int
	err = h.db.QueryRow(`SELECT username, is_admin FROM users WHERE id = ?`, userID).Scan(&username, &isAdmin)
	if err != nil {
		log.Printf("[auth/refresh] utilisateur introuvable — userID=%d ip=%s err=%v", userID, ip, err)
		JSONError(w, "Utilisateur introuvable", http.StatusUnauthorized)
		return
	}

	accessToken, err := h.jwtManager.GenerateAccessToken(userID, username, isAdmin == 1)
	if err != nil {
		JSONError(w, "Erreur génération token", http.StatusInternalServerError)
		return
	}

	JSONResponse(w, http.StatusOK, map[string]any{
		"access_token": accessToken,
		"expires_in":   900,
	})
}

// Me retourne le profil de l'utilisateur connecté.
// GET /api/auth/me
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
	claims := GetClaims(r)
	if claims == nil {
		JSONError(w, "Non authentifié", http.StatusUnauthorized)
		return
	}

	var lang, theme, sidebarPos string
	var lastLogin sql.NullTime
	err := h.db.QueryRow(`SELECT lang, theme, sidebar_position, last_login_at FROM users WHERE id = ?`, claims.UserID).
		Scan(&lang, &theme, &sidebarPos, &lastLogin)
	if err != nil {
		JSONError(w, "Profil introuvable", http.StatusNotFound)
		return
	}

	resp := map[string]any{
		"id":               claims.UserID,
		"username":         claims.Username,
		"is_admin":         claims.IsAdmin,
		"lang":             lang,
		"theme":            theme,
		"sidebar_position": sidebarPos,
	}
	if lastLogin.Valid {
		resp["last_login_at"] = lastLogin.Time
	}

	JSONResponse(w, http.StatusOK, resp)
}

// UpdatePreferences met à jour les préférences de l'utilisateur connecté.
// PATCH /api/auth/preferences
func (h *AuthHandler) UpdatePreferences(w http.ResponseWriter, r *http.Request) {
	claims := GetClaims(r)

	var body struct {
		Lang            *string `json:"lang"`
		Theme           *string `json:"theme"`
		SidebarPosition *string `json:"sidebar_position"`
	}
	if err := decodeJSON(r, &body); err != nil {
		JSONError(w, "Corps de requête invalide", http.StatusBadRequest)
		return
	}

	if body.Lang != nil {
		if !isValidLang(*body.Lang) {
			JSONError(w, "Langue non supportée", http.StatusBadRequest)
			return
		}
		h.db.Exec(`UPDATE users SET lang = ? WHERE id = ?`, *body.Lang, claims.UserID)
	}
	if body.Theme != nil {
		if *body.Theme != "dark" && *body.Theme != "light" {
			JSONError(w, "Thème invalide (dark ou light)", http.StatusBadRequest)
			return
		}
		h.db.Exec(`UPDATE users SET theme = ? WHERE id = ?`, *body.Theme, claims.UserID)
	}
	if body.SidebarPosition != nil {
		if *body.SidebarPosition != "left" && *body.SidebarPosition != "right" {
			JSONError(w, "Position sidebar invalide (left ou right)", http.StatusBadRequest)
			return
		}
		h.db.Exec(`UPDATE users SET sidebar_position = ? WHERE id = ?`, *body.SidebarPosition, claims.UserID)
	}

	JSONResponse(w, http.StatusOK, map[string]string{"message": "Préférences mises à jour"})
}

// upsertUser crée ou met à jour le profil utilisateur en SQLite.
// Retourne l'ID de l'utilisateur.
func (h *AuthHandler) upsertUser(info *auth.UserInfo) (int64, error) {
	isAdmin := 0
	if info.IsAdmin {
		isAdmin = 1
	}

	// Mise à jour du statut admin à chaque connexion (peut changer côté Linux)
	if _, err := h.db.Exec(`
		INSERT INTO users (username, is_admin) VALUES (?, ?)
		ON CONFLICT(username) DO UPDATE SET is_admin = excluded.is_admin
	`, info.Username, isAdmin); err != nil {
		return 0, err
	}

	// Toujours faire un SELECT explicite : avec ON CONFLICT DO UPDATE sur une ligne
	// existante, LastInsertId() peut retourner un rowid obsolète (comportement SQLite).
	var id int64
	if err := h.db.QueryRow(`SELECT id FROM users WHERE username = ?`, info.Username).Scan(&id); err != nil {
		return 0, fmt.Errorf("utilisateur introuvable après upsert: %w", err)
	}
	return id, nil
}

// GetSessions retourne les sessions actives de l'utilisateur connecté.
// GET /api/auth/sessions
func (h *AuthHandler) GetSessions(w http.ResponseWriter, r *http.Request) {
	claims := GetClaims(r)
	if claims == nil {
		JSONError(w, "Non authentifié", http.StatusUnauthorized)
		return
	}

	rows, err := h.db.Query(`
		SELECT id, user_agent, ip, created_at, last_used_at, expires_at, token_hash
		FROM refresh_tokens
		WHERE user_id = ? AND expires_at > CURRENT_TIMESTAMP
		ORDER BY COALESCE(last_used_at, created_at) DESC
	`, claims.UserID)
	if err != nil {
		JSONError(w, "Erreur récupération sessions", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	// Hash du cookie courant pour marquer "session actuelle"
	currentHash := ""
	if cookie, err := r.Cookie("pxp_refresh"); err == nil {
		currentHash = hashToken(cookie.Value)
	}

	type Session struct {
		ID        int64   `json:"id"`
		UserAgent string  `json:"user_agent"`
		IP        string  `json:"ip"`
		CreatedAt string  `json:"created_at"`
		LastUsedAt *string `json:"last_used_at"`
		ExpiresAt string  `json:"expires_at"`
		IsCurrent bool    `json:"is_current"`
	}

	sessions := []Session{}
	for rows.Next() {
		var s Session
		var tokenHash string
		var createdAt, expiresAt sql.NullString
		var lastUsedAt sql.NullString
		if err := rows.Scan(&s.ID, &s.UserAgent, &s.IP, &createdAt, &lastUsedAt, &expiresAt, &tokenHash); err != nil {
			log.Printf("[GetSessions] scan error userID=%d: %v", claims.UserID, err)
			continue
		}
		s.CreatedAt = createdAt.String
		s.ExpiresAt = expiresAt.String
		if lastUsedAt.Valid && lastUsedAt.String != "" {
			s.LastUsedAt = &lastUsedAt.String
		}
		s.IsCurrent = currentHash != "" && tokenHash == currentHash
		sessions = append(sessions, s)
	}

	JSONResponse(w, http.StatusOK, sessions)
}

// RevokeSession révoque une session (refresh token) de l'utilisateur connecté.
// DELETE /api/auth/sessions/{id}
func (h *AuthHandler) RevokeSession(w http.ResponseWriter, r *http.Request) {
	claims := GetClaims(r)
	if claims == nil {
		JSONError(w, "Non authentifié", http.StatusUnauthorized)
		return
	}

	sessionID := chi.URLParam(r, "id")
	res, err := h.db.Exec(`DELETE FROM refresh_tokens WHERE id = ? AND user_id = ?`, sessionID, claims.UserID)
	if err != nil {
		JSONError(w, "Erreur révocation session", http.StatusInternalServerError)
		return
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		JSONError(w, "Session introuvable", http.StatusNotFound)
		return
	}

	h.auditLogger.Log(&claims.UserID, claims.Username, "session_revoked", sessionID, nil, clientIP(r))
	JSONResponse(w, http.StatusOK, map[string]string{"message": "Session révoquée"})
}

// hashToken crée un hash SHA-256 d'un token pour le stockage en base.
func hashToken(token string) string {
	h := sha256.Sum256([]byte(token))
	return hex.EncodeToString(h[:])
}
