package handlers

import (
	"context"
	"errors"
	"net/http"
	"strings"
	"time"

	"github.com/jackc/pgx/v5"

	"pdfeditor/internal/auth"
)

// userResponse représente un utilisateur tel qu'exposé par l'API.
type userResponse struct {
	ID        string    `json:"id"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

// credentials est le corps des requêtes register/login.
type credentials struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

// tokenPair regroupe un couple access/refresh token.
type tokenPair struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
}

// authResponse est la réponse de register/login.
type authResponse struct {
	User         userResponse `json:"user"`
	AccessToken  string       `json:"access_token"`
	RefreshToken string       `json:"refresh_token"`
}

// validateCredentials applique des règles minimales de validation.
func validateCredentials(c credentials) (string, bool) {
	email := strings.TrimSpace(strings.ToLower(c.Email))
	if email == "" || !strings.Contains(email, "@") || len(email) > 254 {
		return "", false
	}
	if len(c.Password) < 8 || len(c.Password) > 128 {
		return "", false
	}
	return email, true
}

// Register crée un nouvel utilisateur et renvoie un couple de tokens.
func (a *API) Register(w http.ResponseWriter, r *http.Request) {
	var creds credentials
	if !decodeJSON(w, r, &creds) {
		return
	}
	email, ok := validateCredentials(creds)
	if !ok {
		writeError(w, http.StatusBadRequest, "email invalide ou mot de passe trop court (8 caractères minimum)")
		return
	}

	hash, err := auth.HashPassword(creds.Password)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "erreur interne")
		return
	}

	ctx := r.Context()
	var user userResponse
	err = a.DB.QueryRow(ctx,
		`INSERT INTO users (email, password_hash) VALUES ($1, $2)
		 RETURNING id, email, created_at`,
		email, hash,
	).Scan(&user.ID, &user.Email, &user.CreatedAt)
	if err != nil {
		// Violation de contrainte d'unicité (email déjà pris).
		if strings.Contains(err.Error(), "users_email_key") || strings.Contains(err.Error(), "duplicate key") {
			writeError(w, http.StatusConflict, "cet email est déjà utilisé")
			return
		}
		writeError(w, http.StatusInternalServerError, "erreur lors de la création du compte")
		return
	}

	pair, err := a.issueTokens(ctx, user.ID)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "erreur lors de la génération des tokens")
		return
	}

	writeJSON(w, http.StatusCreated, authResponse{User: user, AccessToken: pair.AccessToken, RefreshToken: pair.RefreshToken})
}

// Login authentifie un utilisateur et renvoie un couple de tokens.
func (a *API) Login(w http.ResponseWriter, r *http.Request) {
	var creds credentials
	if !decodeJSON(w, r, &creds) {
		return
	}
	email := strings.TrimSpace(strings.ToLower(creds.Email))
	if email == "" || creds.Password == "" {
		writeError(w, http.StatusBadRequest, "email et mot de passe requis")
		return
	}

	ctx := r.Context()
	var user userResponse
	var passwordHash string
	err := a.DB.QueryRow(ctx,
		`SELECT id, email, created_at, password_hash FROM users WHERE email = $1`,
		email,
	).Scan(&user.ID, &user.Email, &user.CreatedAt, &passwordHash)
	if err != nil {
		// On ne distingue pas "email inconnu" de "mauvais mot de passe" (anti-énumération).
		writeError(w, http.StatusUnauthorized, "identifiants invalides")
		return
	}

	if !auth.CheckPassword(passwordHash, creds.Password) {
		writeError(w, http.StatusUnauthorized, "identifiants invalides")
		return
	}

	pair, err := a.issueTokens(ctx, user.ID)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "erreur lors de la génération des tokens")
		return
	}

	writeJSON(w, http.StatusOK, authResponse{User: user, AccessToken: pair.AccessToken, RefreshToken: pair.RefreshToken})
}

// refreshRequest est le corps de la requête de rafraîchissement.
type refreshRequest struct {
	RefreshToken string `json:"refresh_token"`
}

// Refresh échange un refresh token valide contre un nouveau couple de tokens.
// L'ancien refresh token est révoqué (rotation).
func (a *API) Refresh(w http.ResponseWriter, r *http.Request) {
	var req refreshRequest
	if !decodeJSON(w, r, &req) {
		return
	}
	if req.RefreshToken == "" {
		writeError(w, http.StatusBadRequest, "refresh_token requis")
		return
	}

	ctx := r.Context()
	hash := auth.HashToken(req.RefreshToken)

	var tokenID, userID string
	var expiresAt time.Time
	var revoked bool
	err := a.DB.QueryRow(ctx,
		`SELECT id, user_id, expires_at, revoked FROM refresh_tokens WHERE token_hash = $1`,
		hash,
	).Scan(&tokenID, &userID, &expiresAt, &revoked)
	if err != nil {
		writeError(w, http.StatusUnauthorized, "refresh token invalide")
		return
	}
	if revoked || time.Now().After(expiresAt) {
		writeError(w, http.StatusUnauthorized, "refresh token expiré ou révoqué")
		return
	}

	// Rotation : on révoque l'ancien token avant d'en émettre un nouveau.
	if _, err := a.DB.Exec(ctx, `UPDATE refresh_tokens SET revoked = TRUE WHERE id = $1`, tokenID); err != nil {
		writeError(w, http.StatusInternalServerError, "erreur interne")
		return
	}

	pair, err := a.issueTokens(ctx, userID)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "erreur lors de la génération des tokens")
		return
	}

	writeJSON(w, http.StatusOK, pair)
}

// Logout révoque le refresh token fourni. Auth requise.
func (a *API) Logout(w http.ResponseWriter, r *http.Request) {
	var req refreshRequest
	// Le refresh token peut être fourni dans le corps ; s'il est absent, on
	// révoque malgré tout tous les tokens de l'utilisateur courant.
	_ = decodeJSON(w, r, &req)
	userID := userIDFromContext(r.Context())

	ctx := r.Context()
	if req.RefreshToken != "" {
		hash := auth.HashToken(req.RefreshToken)
		_, err := a.DB.Exec(ctx,
			`UPDATE refresh_tokens SET revoked = TRUE WHERE token_hash = $1 AND user_id = $2`,
			hash, userID)
		if err != nil {
			writeError(w, http.StatusInternalServerError, "erreur interne")
			return
		}
	} else {
		// Pas de token précisé : on révoque toutes les sessions de l'utilisateur.
		if _, err := a.DB.Exec(ctx,
			`UPDATE refresh_tokens SET revoked = TRUE WHERE user_id = $1 AND revoked = FALSE`,
			userID); err != nil {
			writeError(w, http.StatusInternalServerError, "erreur interne")
			return
		}
	}

	w.WriteHeader(http.StatusNoContent)
}

// Me renvoie le profil de l'utilisateur authentifié.
func (a *API) Me(w http.ResponseWriter, r *http.Request) {
	userID := userIDFromContext(r.Context())
	var user userResponse
	err := a.DB.QueryRow(r.Context(),
		`SELECT id, email, created_at FROM users WHERE id = $1`, userID,
	).Scan(&user.ID, &user.Email, &user.CreatedAt)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			writeError(w, http.StatusNotFound, "utilisateur introuvable")
			return
		}
		writeError(w, http.StatusInternalServerError, "erreur interne")
		return
	}
	writeJSON(w, http.StatusOK, user)
}

// issueTokens génère un access token et un refresh token, persiste ce dernier
// (hashé) en base, et retourne le couple.
func (a *API) issueTokens(ctx context.Context, userID string) (tokenPair, error) {
	accessToken, err := a.Auth.GenerateAccessToken(userID)
	if err != nil {
		return tokenPair{}, err
	}

	refreshToken, refreshHash, expiresAt, err := a.Auth.GenerateRefreshToken()
	if err != nil {
		return tokenPair{}, err
	}

	_, err = a.DB.Exec(ctx,
		`INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)`,
		userID, refreshHash, expiresAt)
	if err != nil {
		return tokenPair{}, err
	}

	return tokenPair{AccessToken: accessToken, RefreshToken: refreshToken}, nil
}
